mirror of
https://github.com/inventree/InvenTree.git
synced 2026-09-01 09:41:22 +00:00
Merge branch 'master' into block-notes
This commit is contained in:
@@ -520,9 +520,9 @@ defusedxml==0.7.1 \
|
||||
# via
|
||||
# -c src/backend/requirements.txt
|
||||
# python3-openid
|
||||
django==5.2.14 \
|
||||
--hash=sha256:58a63ba841662e5c686b57ba1fec52ddd68c0b93bd96ac3029d55728f00bf8a2 \
|
||||
--hash=sha256:6f712143bd3064310d1f50fac859c3e9a274bdcfc9595339853be7779297fc76
|
||||
django==5.2.15 \
|
||||
--hash=sha256:0eb4a9bb1853a35b0286dbc6d916bd352c8c2687195a7f2d6f80cefd840e4970 \
|
||||
--hash=sha256:5154a9bf84ac01dde011e367f355c07dbb329532e06810dcf3ef2af269e236e7
|
||||
# via
|
||||
# -c src/backend/requirements.txt
|
||||
# -r src/backend/requirements.in
|
||||
|
||||
@@ -408,9 +408,9 @@ cryptography==48.0.0 \
|
||||
# -c src/backend/requirements-3.14.txt
|
||||
# -c src/backend/requirements.txt
|
||||
# pdfminer-six
|
||||
django==5.2.14 \
|
||||
--hash=sha256:58a63ba841662e5c686b57ba1fec52ddd68c0b93bd96ac3029d55728f00bf8a2 \
|
||||
--hash=sha256:6f712143bd3064310d1f50fac859c3e9a274bdcfc9595339853be7779297fc76
|
||||
django==5.2.15 \
|
||||
--hash=sha256:0eb4a9bb1853a35b0286dbc6d916bd352c8c2687195a7f2d6f80cefd840e4970 \
|
||||
--hash=sha256:5154a9bf84ac01dde011e367f355c07dbb329532e06810dcf3ef2af269e236e7
|
||||
# via
|
||||
# -c src/backend/requirements-3.14.txt
|
||||
# -c src/backend/requirements.txt
|
||||
|
||||
@@ -403,9 +403,9 @@ cryptography==48.0.0 \
|
||||
# via
|
||||
# -c src/backend/requirements.txt
|
||||
# pdfminer-six
|
||||
django==5.2.14 \
|
||||
--hash=sha256:58a63ba841662e5c686b57ba1fec52ddd68c0b93bd96ac3029d55728f00bf8a2 \
|
||||
--hash=sha256:6f712143bd3064310d1f50fac859c3e9a274bdcfc9595339853be7779297fc76
|
||||
django==5.2.15 \
|
||||
--hash=sha256:0eb4a9bb1853a35b0286dbc6d916bd352c8c2687195a7f2d6f80cefd840e4970 \
|
||||
--hash=sha256:5154a9bf84ac01dde011e367f355c07dbb329532e06810dcf3ef2af269e236e7
|
||||
# via
|
||||
# -c src/backend/requirements.txt
|
||||
# django-silk
|
||||
|
||||
@@ -501,9 +501,9 @@ defusedxml==0.7.1 \
|
||||
--hash=sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69 \
|
||||
--hash=sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61
|
||||
# via python3-openid
|
||||
django==5.2.14 \
|
||||
--hash=sha256:58a63ba841662e5c686b57ba1fec52ddd68c0b93bd96ac3029d55728f00bf8a2 \
|
||||
--hash=sha256:6f712143bd3064310d1f50fac859c3e9a274bdcfc9595339853be7779297fc76
|
||||
django==5.2.15 \
|
||||
--hash=sha256:0eb4a9bb1853a35b0286dbc6d916bd352c8c2687195a7f2d6f80cefd840e4970 \
|
||||
--hash=sha256:5154a9bf84ac01dde011e367f355c07dbb329532e06810dcf3ef2af269e236e7
|
||||
# via
|
||||
# -r src/backend/requirements.in
|
||||
# django-allauth
|
||||
|
||||
@@ -27,6 +27,10 @@ Exposes sub-components related to DetailDrawer rendering:
|
||||
- `DetailDrawerComponent`
|
||||
- `useLocalLibState`
|
||||
|
||||
#### Plugin System
|
||||
|
||||
Enable React Fast Refresh support for plugin frontend development. Plugin modules exporting React components must start with a capital letter; otherwise, a full page reload occurs instead of a component-level update.
|
||||
|
||||
### 0.11.3 - April 2026
|
||||
|
||||
Exposes additional type definitions related to rendering drawers from tables:
|
||||
|
||||
@@ -3,9 +3,6 @@ import { defineConfig, devices } from '@playwright/test';
|
||||
// Detect if running in CI
|
||||
const IS_CI = !!process.env.CI;
|
||||
|
||||
const MAX_WORKERS: number = 3;
|
||||
const MAX_RETRIES: number = 3;
|
||||
|
||||
/* We optionally spin-up services based on the testing mode:
|
||||
*
|
||||
* Local Development:
|
||||
@@ -26,21 +23,28 @@ const MAX_RETRIES: number = 3;
|
||||
* - WORKERS = 1 (to avoid conflicts with HMR)
|
||||
*/
|
||||
|
||||
const BASE_URL: string = IS_CI
|
||||
? 'http://localhost:8000'
|
||||
: 'http://localhost:5173';
|
||||
const BASE_URL: string =
|
||||
process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:5173';
|
||||
|
||||
// If running in "production" mode, we can use multiple workers to speed up the tests
|
||||
const MAX_WORKERS: number = BASE_URL.endsWith('8000') ? 3 : 1;
|
||||
const MAX_RETRIES: number = IS_CI ? 1 : 2;
|
||||
|
||||
console.log('Running Playwright Tests:');
|
||||
console.log('- Base URL:', BASE_URL);
|
||||
console.log('- Max Workers:', MAX_WORKERS);
|
||||
console.log('- Max Retries:', MAX_RETRIES);
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './tests',
|
||||
fullyParallel: false,
|
||||
timeout: 90000,
|
||||
forbidOnly: !!IS_CI,
|
||||
retries: IS_CI ? MAX_RETRIES : 0,
|
||||
workers: IS_CI ? MAX_WORKERS : 1,
|
||||
reporter: IS_CI ? [['html', { open: 'never' }], ['github']] : 'list',
|
||||
retries: MAX_RETRIES,
|
||||
workers: MAX_WORKERS,
|
||||
reporter: IS_CI
|
||||
? [['html', { open: 'never' }], ['blob'], ['github']]
|
||||
: 'list',
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
@@ -57,13 +61,6 @@ export default defineConfig({
|
||||
...devices['Desktop Firefox']
|
||||
},
|
||||
testIgnore: /customization/ // Ignore all tests in the "customization" folder for this project
|
||||
},
|
||||
{
|
||||
name: 'customization',
|
||||
use: {
|
||||
...devices['Desktop Firefox']
|
||||
},
|
||||
testIgnore: /pui_.*\.spec\.ts/ // Ignore all "pui_*.spec.ts" tests for this project
|
||||
}
|
||||
],
|
||||
|
||||
@@ -89,7 +86,9 @@ export default defineConfig({
|
||||
INVENTREE_CORS_ORIGIN_ALLOW_ALL: 'True',
|
||||
INVENTREE_COOKIE_SAMESITE: 'False',
|
||||
INVENTREE_LOGIN_ATTEMPTS: '100',
|
||||
INVENTREE_PLUGINS_MANDATORY: 'samplelocate'
|
||||
INVENTREE_PLUGINS_MANDATORY: 'samplelocate',
|
||||
INVENTREE_CUSTOM_SPLASH: 'img/playwright_custom_splash.png',
|
||||
INVENTREE_CUSTOM_LOGO: 'img/playwright_custom_logo.png'
|
||||
},
|
||||
url: 'http://localhost:8000/api/',
|
||||
reuseExistingServer: IS_CI,
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { Alert, MantineProvider, Stack, Text } from '@mantine/core';
|
||||
import { IconExclamationCircle } from '@tabler/icons-react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Alert, Stack, Text } from '@mantine/core';
|
||||
import { useRef } from 'react';
|
||||
|
||||
import { Boundary } from '@lib/components/Boundary';
|
||||
import { identifierString } from '@lib/functions/Conversion';
|
||||
import type { InvenTreePluginContext } from '@lib/types/Plugins';
|
||||
import { type Root, createRoot } from 'react-dom/client';
|
||||
import { api, queryClient } from '../../App';
|
||||
import { ApiProvider } from '../../contexts/ApiContext';
|
||||
import { LanguageContext } from '../../contexts/LanguageContext';
|
||||
import { useLocalState } from '../../states/LocalState';
|
||||
import { findExternalPluginFunction } from './PluginSource';
|
||||
import { IconExclamationCircle } from '@tabler/icons-react';
|
||||
import { useRemotePlugin } from '../../hooks/UseRemotePlugin';
|
||||
|
||||
/**
|
||||
* A remote component which can be used to display plugin content.
|
||||
@@ -31,125 +26,39 @@ export default function RemoteComponent({
|
||||
defaultFunctionName: string;
|
||||
context: InvenTreePluginContext;
|
||||
}>) {
|
||||
const componentRef = useRef<HTMLDivElement | null>(null);
|
||||
const rootElement = useRef<Root | null>(null);
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (componentRef.current && rootElement.current === null) {
|
||||
rootElement.current = createRoot(componentRef.current);
|
||||
}
|
||||
}, [rootElement]);
|
||||
const { componentFn, errorMsg, exportName, pluginContext, remountKey } =
|
||||
useRemotePlugin({
|
||||
context,
|
||||
source,
|
||||
defaultFunctionName,
|
||||
containerRef
|
||||
});
|
||||
|
||||
const [renderingError, setRenderingError] = useState<string | undefined>(
|
||||
undefined
|
||||
const content = componentFn ? (
|
||||
componentFn(pluginContext)
|
||||
) : (
|
||||
<div ref={containerRef} />
|
||||
);
|
||||
|
||||
const func: string = useMemo(() => {
|
||||
// Attempt to extract the function name from the source
|
||||
const { getHost } = useLocalState.getState();
|
||||
const url = new URL(source, getHost());
|
||||
|
||||
if (url.pathname.includes(':')) {
|
||||
const parts = url.pathname.split(':');
|
||||
return parts[1] || defaultFunctionName; // Use the second part as the function name, or fallback to default
|
||||
} else {
|
||||
return defaultFunctionName;
|
||||
}
|
||||
}, [source, defaultFunctionName]);
|
||||
|
||||
const reloadPluginContent = useCallback(() => {
|
||||
if (!rootElement.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ctx: InvenTreePluginContext = {
|
||||
...context,
|
||||
reloadContent: reloadPluginContent
|
||||
};
|
||||
|
||||
if (source && defaultFunctionName) {
|
||||
findExternalPluginFunction(source, func)
|
||||
.then((func) => {
|
||||
if (!!func) {
|
||||
try {
|
||||
if (func.length > 1) {
|
||||
// Support "legacy" plugin functions which call createRoot() internally
|
||||
// Ref: https://github.com/inventree/InvenTree/pull/9439/
|
||||
func(componentRef.current, ctx);
|
||||
} else {
|
||||
// Render the plugin component into the target element
|
||||
// Note that we have to provide the right context(s) to the component
|
||||
// This approach ensures that the component is rendered in the correct context tree
|
||||
rootElement.current?.render(
|
||||
<ApiProvider client={queryClient} api={api}>
|
||||
<MantineProvider
|
||||
theme={ctx.theme}
|
||||
defaultColorScheme={ctx.colorScheme}
|
||||
>
|
||||
<LanguageContext>{func(ctx)}</LanguageContext>
|
||||
</MantineProvider>
|
||||
</ApiProvider>
|
||||
);
|
||||
}
|
||||
|
||||
setRenderingError('');
|
||||
} catch (error) {
|
||||
setRenderingError(`${error}`);
|
||||
console.error(error);
|
||||
}
|
||||
} else {
|
||||
setRenderingError(`${source} / ${func}`);
|
||||
}
|
||||
})
|
||||
.catch((_error) => {
|
||||
console.error(
|
||||
`ERR: Failed to load remote plugin function: ${source} /${func}`
|
||||
);
|
||||
});
|
||||
} else {
|
||||
setRenderingError(
|
||||
`${t`Invalid source or function name`} - ${source} /${func}`
|
||||
);
|
||||
}
|
||||
}, [
|
||||
componentRef.current,
|
||||
rootElement.current,
|
||||
source,
|
||||
defaultFunctionName,
|
||||
context
|
||||
]);
|
||||
|
||||
// Reload the plugin content dynamically
|
||||
useEffect(() => {
|
||||
reloadPluginContent();
|
||||
}, [
|
||||
func,
|
||||
rootElement.current,
|
||||
context.id,
|
||||
context.model,
|
||||
context.instance,
|
||||
context.user,
|
||||
context.colorScheme,
|
||||
context.locale,
|
||||
context.context
|
||||
]);
|
||||
|
||||
return (
|
||||
<Boundary label={identifierString(`RemoteComponent-${func}`)}>
|
||||
<Stack gap='xs'>
|
||||
{renderingError && (
|
||||
<Alert
|
||||
color='red'
|
||||
title={t`Error Loading Content`}
|
||||
icon={<IconExclamationCircle />}
|
||||
>
|
||||
<Text>
|
||||
{t`Error occurred while loading plugin content`}: {renderingError}
|
||||
</Text>
|
||||
</Alert>
|
||||
)}
|
||||
{componentRef && <div ref={componentRef as any} />}
|
||||
</Stack>
|
||||
</Boundary>
|
||||
<Stack>
|
||||
{errorMsg && (
|
||||
<Alert
|
||||
color='red'
|
||||
title={t`Error Loading Plugin Content`}
|
||||
icon={<IconExclamationCircle />}
|
||||
>
|
||||
<Text>{errorMsg}</Text>
|
||||
</Alert>
|
||||
)}
|
||||
<Boundary
|
||||
key={remountKey}
|
||||
label={identifierString(`RemoteComponent-${exportName}`)}
|
||||
>
|
||||
{content}
|
||||
</Boundary>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
import type { InvenTreePluginContext } from '@lib/types/Plugins';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import type { ReactElement } from 'react';
|
||||
import { useLocalState } from '../states/LocalState';
|
||||
|
||||
type LegacyPluginEntryFn = (
|
||||
container: HTMLDivElement,
|
||||
ctx: InvenTreePluginContext
|
||||
) => void;
|
||||
|
||||
type PluginEntryFn = (ctx: InvenTreePluginContext) => ReactElement;
|
||||
|
||||
type UseRemotePluginOptions = {
|
||||
context: InvenTreePluginContext;
|
||||
source: string;
|
||||
defaultFunctionName: string;
|
||||
containerRef: React.RefObject<HTMLDivElement | null>;
|
||||
};
|
||||
|
||||
type UsePluginSourceOptions = {
|
||||
source: string;
|
||||
defaultFunctionName?: string;
|
||||
};
|
||||
|
||||
type UseRemotePluginReturn = {
|
||||
componentFn: PluginEntryFn | null;
|
||||
errorMsg: string | null;
|
||||
exportName: string;
|
||||
pluginContext: InvenTreePluginContext;
|
||||
remountKey: number;
|
||||
};
|
||||
|
||||
function usePluginSource({
|
||||
source,
|
||||
defaultFunctionName
|
||||
}: UsePluginSourceOptions) {
|
||||
const { getHost } = useLocalState.getState();
|
||||
|
||||
const { moduleUrl, exportName } = useMemo(() => {
|
||||
const url = new URL(source, getHost());
|
||||
const parts = url.pathname.split(':');
|
||||
|
||||
return {
|
||||
exportName: parts[1] || defaultFunctionName || 'default',
|
||||
moduleUrl: url.origin + parts[0]
|
||||
};
|
||||
}, [source, defaultFunctionName, getHost]);
|
||||
|
||||
return { moduleUrl, exportName };
|
||||
}
|
||||
|
||||
function getHmrCallbacks(url: string) {
|
||||
const w = window as any;
|
||||
w.__plugin_hmr_callbacks ??= {};
|
||||
w.__plugin_hmr_callbacks[url] ??= new Set<Function>();
|
||||
return w.__plugin_hmr_callbacks[url];
|
||||
}
|
||||
|
||||
const hasHmr = import.meta.hot !== undefined;
|
||||
|
||||
export function useRemotePlugin({
|
||||
context,
|
||||
source,
|
||||
defaultFunctionName,
|
||||
containerRef
|
||||
}: UseRemotePluginOptions): UseRemotePluginReturn {
|
||||
const { moduleUrl, exportName } = usePluginSource({
|
||||
source,
|
||||
defaultFunctionName
|
||||
});
|
||||
|
||||
const [remoteModule, setRemoteModule] = useState<Record<
|
||||
string,
|
||||
unknown
|
||||
> | null>(null);
|
||||
const [reloadVersion, setReloadVersion] = useState(0);
|
||||
const [errorMsg, setErrorMsg] = useState<string | null>(null);
|
||||
|
||||
const reloadContent = useCallback(() => setReloadVersion((v) => v + 1), []);
|
||||
|
||||
const hmrSetModule = useCallback(
|
||||
(newRemoteModule: Record<string, unknown> | null) => {
|
||||
if (!hasHmr) return;
|
||||
setRemoteModule(newRemoteModule);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
setErrorMsg(null);
|
||||
|
||||
const loadModule = async () => {
|
||||
try {
|
||||
const mod = await import(/* @vite-ignore */ moduleUrl);
|
||||
if (!cancelled) setRemoteModule(mod);
|
||||
} catch (err) {
|
||||
if (!cancelled) {
|
||||
console.error(`ERR: Failed to load module: ${moduleUrl}:\n${err}`);
|
||||
setErrorMsg(t`Failed to load module: ${moduleUrl}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadModule();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [moduleUrl]);
|
||||
|
||||
const [legacyRenderFn, componentFn, error] = useMemo(() => {
|
||||
if (!remoteModule) return [null, null, null];
|
||||
|
||||
let err: string | null = null;
|
||||
const func = remoteModule[exportName];
|
||||
|
||||
if (typeof func === 'function') {
|
||||
if (func.length === 2) {
|
||||
return [func as LegacyPluginEntryFn, null, null];
|
||||
} else if (func.length === 1) {
|
||||
return [null, func as PluginEntryFn, null];
|
||||
} else {
|
||||
err = `Entrypoint ${exportName} in ${moduleUrl} must accept 1-2 arguments`;
|
||||
}
|
||||
} else if (func !== undefined) {
|
||||
err = t`Export ${exportName} in ${moduleUrl} is not a function (found type ${typeof func}).`;
|
||||
} else {
|
||||
err = t`Plugin entrypoint ${exportName} does not exist in ${moduleUrl}.`;
|
||||
}
|
||||
|
||||
return [null, null, err];
|
||||
}, [remoteModule, exportName]);
|
||||
|
||||
useEffect(() => {
|
||||
if (legacyRenderFn && containerRef.current) {
|
||||
containerRef.current.innerHTML = '';
|
||||
legacyRenderFn(containerRef.current, context);
|
||||
|
||||
if (hasHmr) getHmrCallbacks(moduleUrl)?.add(hmrSetModule);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (hasHmr) getHmrCallbacks(moduleUrl)?.delete(hmrSetModule);
|
||||
};
|
||||
}, [moduleUrl, legacyRenderFn, context, hmrSetModule]);
|
||||
|
||||
return {
|
||||
componentFn: componentFn,
|
||||
errorMsg: error ?? errorMsg,
|
||||
exportName: exportName,
|
||||
pluginContext: { ...context, reloadContent: reloadContent },
|
||||
remountKey: reloadVersion
|
||||
};
|
||||
}
|
||||
@@ -2,9 +2,14 @@ import * as crypto from 'node:crypto';
|
||||
import * as fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import { test as baseTest } from '@playwright/test';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { type BrowserContext, test as baseTest } from '@playwright/test';
|
||||
|
||||
const istanbulCLIOutput = path.join(process.cwd(), '.nyc_output');
|
||||
const frontendDir = path.resolve(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
'..'
|
||||
);
|
||||
const istanbulCLIOutput = path.join(frontendDir, '.nyc_output');
|
||||
const platform = os.platform();
|
||||
let systemKeyVar: string;
|
||||
if (platform === 'darwin') {
|
||||
@@ -19,16 +24,16 @@ export function generateUUID(): string {
|
||||
return crypto.randomBytes(16).toString('hex');
|
||||
}
|
||||
|
||||
export const test = baseTest.extend({
|
||||
context: async ({ context }, use) => {
|
||||
await context.addInitScript(() =>
|
||||
window.addEventListener('beforeunload', () =>
|
||||
(window as any).collectIstanbulCoverage(
|
||||
JSON.stringify((window as any).__coverage__)
|
||||
)
|
||||
async function setupCoverageCollection(context: BrowserContext) {
|
||||
await context.addInitScript(() =>
|
||||
window.addEventListener('beforeunload', () =>
|
||||
(window as any).collectIstanbulCoverage?.(
|
||||
JSON.stringify((window as any).__coverage__)
|
||||
)
|
||||
);
|
||||
await fs.promises.mkdir(istanbulCLIOutput, { recursive: true });
|
||||
)
|
||||
);
|
||||
await fs.promises.mkdir(istanbulCLIOutput, { recursive: true });
|
||||
try {
|
||||
await context.exposeFunction(
|
||||
'collectIstanbulCoverage',
|
||||
(coverageJSON: string) => {
|
||||
@@ -42,18 +47,68 @@ export const test = baseTest.extend({
|
||||
);
|
||||
}
|
||||
);
|
||||
} catch {
|
||||
// already exposed on this context (e.g. called twice for same context)
|
||||
}
|
||||
}
|
||||
|
||||
async function collectCoverageFromContext(context: BrowserContext) {
|
||||
await Promise.allSettled(
|
||||
context.pages().map(async (page) => {
|
||||
try {
|
||||
await Promise.race([
|
||||
page.evaluate(() =>
|
||||
(window as any).collectIstanbulCoverage?.(
|
||||
JSON.stringify((window as any).__coverage__)
|
||||
)
|
||||
),
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(
|
||||
() => reject(new Error('Coverage collection timeout')),
|
||||
2000
|
||||
)
|
||||
)
|
||||
]);
|
||||
} catch {
|
||||
// page may already be closed or script execution can be blocked during teardown
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export const test = baseTest.extend<{}, {}>({
|
||||
// Wrap browser.newPage so contexts created via doCachedLogin also get coverage
|
||||
browser: [
|
||||
async ({ browser }, use) => {
|
||||
const origNewPage = browser.newPage.bind(browser);
|
||||
(browser as any).newPage = async (
|
||||
options?: Parameters<typeof browser.newPage>[0]
|
||||
) => {
|
||||
const page = await origNewPage(options);
|
||||
await setupCoverageCollection(page.context());
|
||||
return page;
|
||||
};
|
||||
try {
|
||||
await use(browser);
|
||||
} finally {
|
||||
(browser as any).newPage = origNewPage;
|
||||
for (const context of browser.contexts()) {
|
||||
await collectCoverageFromContext(context);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ scope: 'worker' }
|
||||
],
|
||||
|
||||
context: async ({ context }, use) => {
|
||||
await setupCoverageCollection(context);
|
||||
await use(context);
|
||||
for (const page of context.pages()) {
|
||||
await page.evaluate(() =>
|
||||
(window as any).collectIstanbulCoverage(
|
||||
JSON.stringify((window as any).__coverage__)
|
||||
)
|
||||
);
|
||||
}
|
||||
await collectCoverageFromContext(context);
|
||||
},
|
||||
|
||||
// Ensure no errors are thrown in the console
|
||||
page: async ({ page }, use) => {
|
||||
const messages = [];
|
||||
const messages: any[] = [];
|
||||
page.on('console', (msg) => {
|
||||
const url = msg.location().url;
|
||||
if (
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import test, { expect } from '@playwright/test';
|
||||
import { expect, test } from '../baseFixtures';
|
||||
import { navigate } from '../helpers';
|
||||
|
||||
/**
|
||||
* Tests for user interface customization functionality.
|
||||
*
|
||||
* Note: The correct environment variables must be set for these tests to work correctly. See "playwright.config.ts" for details.
|
||||
* These tests are designed to run in CI environments where specific environment variables are set to enable custom logos and splash screens. The tests verify that these customizations are correctly applied in the user interface.
|
||||
* These tests are designed to run in CI environments where specific environment variables are set to enable custom logos and splash screens.
|
||||
* The tests verify that these customizations are correctly applied in the user interface.
|
||||
*
|
||||
* If you are running these tests locally, ensure that you have the appropriate environment variables set to enable the customizations.
|
||||
* You may need to modify the "webServer" configuration in "playwright.config.ts" to include the necessary environment variables for local testing.
|
||||
*
|
||||
*/
|
||||
|
||||
test('Customization - Splash', async ({ page }) => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import test from '@playwright/test';
|
||||
import { test } from '../baseFixtures';
|
||||
import { loadTab } from '../helpers';
|
||||
import { doCachedLogin } from '../login';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import test from '@playwright/test';
|
||||
import { test } from './baseFixtures';
|
||||
import { stevenuser } from './defaults';
|
||||
import { globalSearch, loadTab, navigate } from './helpers';
|
||||
import { doCachedLogin } from './login';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** Unit tests for form validation, rendering, etc */
|
||||
import { expect, test } from 'playwright/test';
|
||||
import { createApi } from './api';
|
||||
/** Unit tests for form validation, rendering, etc */
|
||||
import { expect, test } from './baseFixtures';
|
||||
import { stevenuser } from './defaults';
|
||||
import { navigate } from './helpers';
|
||||
import { doCachedLogin } from './login';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import test from '@playwright/test';
|
||||
import { test } from './baseFixtures';
|
||||
import { stevenuser } from './defaults';
|
||||
import { doCachedLogin } from './login';
|
||||
|
||||
|
||||
@@ -13,7 +13,8 @@ test('Login - Failures', async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'Log In' }).click();
|
||||
await page.getByText('Login failed', { exact: true }).waitFor();
|
||||
await page.getByText('Check your input and try again').first().waitFor();
|
||||
await page.locator('#login').getByRole('button').click();
|
||||
|
||||
await page.reload();
|
||||
};
|
||||
|
||||
// Navigate to the 'login' page
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import test from 'playwright/test';
|
||||
import { test } from './baseFixtures';
|
||||
import { adminuser } from './defaults';
|
||||
import { clickOnRowMenu, navigate } from './helpers';
|
||||
import { doCachedLogin } from './login';
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Tests for UI permissions checks
|
||||
*/
|
||||
|
||||
import test from '@playwright/test';
|
||||
import { test } from './baseFixtures';
|
||||
import { adminuser, readeruser } from './defaults';
|
||||
import { clickOnRowMenu, loadTab } from './helpers';
|
||||
import { doCachedLogin } from './login';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import test from 'playwright/test';
|
||||
import { test } from './baseFixtures';
|
||||
|
||||
import { adminuser } from './defaults.js';
|
||||
import {
|
||||
@@ -174,7 +174,7 @@ test('Plugins - Panels', async ({ browser }) => {
|
||||
|
||||
// Check out each of the plugin panels
|
||||
await loadTab(page, 'Broken Panel');
|
||||
await page.getByText('Error occurred while loading plugin content').waitFor();
|
||||
await page.getByText('Error Loading Plugin Content').waitFor();
|
||||
await loadTab(page, 'Dynamic Panel');
|
||||
await page.getByText('Instance ID: 69');
|
||||
await page
|
||||
|
||||
@@ -47,7 +47,7 @@ export default defineConfig(({ command, mode }) => {
|
||||
}),
|
||||
istanbul({
|
||||
include: ['src/*', 'lib/*'],
|
||||
exclude: ['node_modules', 'test/'],
|
||||
exclude: ['node_modules/', 'playwright/', 'tests/'],
|
||||
extension: ['.js', '.ts', '.tsx'],
|
||||
requireEnv: true
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user