mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-06-11 16:34:57 +03:00
Merge branch 'clipboard-preserve' into dev
This commit is contained in:
@@ -531,7 +531,7 @@ On error (auto-screenshot taken):
|
|||||||
- **Headed mode** — 1C requires visible browser, no headless
|
- **Headed mode** — 1C requires visible browser, no headless
|
||||||
- **Startup time** — 1C loads 30-60s on initial connect (built into `start`)
|
- **Startup time** — 1C loads 30-60s on initial connect (built into `start`)
|
||||||
- **Fuzzy matching** — all name lookups: exact > startsWith > includes
|
- **Fuzzy matching** — all name lookups: exact > startsWith > includes
|
||||||
- **Clipboard paste** — all text fields filled via Ctrl+V (triggers 1C events properly)
|
- **Clipboard paste** — all text fields filled via Ctrl+V (triggers 1C events properly). The OS clipboard is automatically saved before each action and restored after, so a local user's clipboard survives a test run. Opt out with `--no-preserve-clipboard` (any command), `WEB_TEST_PRESERVE_CLIPBOARD=0` env, or `preserveClipboard: false` in `webtest.config.mjs`
|
||||||
- **Cyrillic in bash** — use `cat <<'SCRIPT' | node $RUN exec -` to avoid escaping issues
|
- **Cyrillic in bash** — use `cat <<'SCRIPT' | node $RUN exec -` to avoid escaping issues
|
||||||
- **Non-breaking spaces** — 1C uses `\u00a0` instead of regular spaces. All matching is normalized internally
|
- **Non-breaking spaces** — 1C uses `\u00a0` instead of regular spaces. All matching is normalized internally
|
||||||
- **Section panel display** — `navigateSection()` works with any panel position (side, top) but requires "Picture and text" or "Text" display mode. Icon-only mode is not supported — API cannot read section names from icons alone
|
- **Section panel display** — `navigateSection()` works with any panel position (side, top) but requires "Picture and text" or "Text" display mode. Icon-only mode is not supported — API cannot read section names from icons alone
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// web-test browser v1.12 — Playwright browser management for 1C web client
|
// web-test browser v1.16 — Playwright browser management for 1C web client
|
||||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
/**
|
/**
|
||||||
* Playwright browser management for 1C web client.
|
* Playwright browser management for 1C web client.
|
||||||
@@ -61,6 +61,98 @@ const STABLE_CYCLES = 3; // consecutive stable cycles needed
|
|||||||
const EXT_ID = 'pbhelknnhilelbnhfpcjlcabhmfangik';
|
const EXT_ID = 'pbhelknnhilelbnhfpcjlcabhmfangik';
|
||||||
let persistentUserDataDir = null; // temp dir for launchPersistentContext, cleaned on disconnect
|
let persistentUserDataDir = null; // temp dir for launchPersistentContext, cleaned on disconnect
|
||||||
|
|
||||||
|
// Clipboard preservation: save full clipboard contents (all MIME types) right before
|
||||||
|
// each writeText+Ctrl+V pair, restore right after — narrow window so a user's
|
||||||
|
// concurrent Ctrl+C isn't clobbered. Blobs are stashed on `window` (no CDP
|
||||||
|
// serialization). Toggled via setPreserveClipboard() from run.mjs.
|
||||||
|
let preserveClipboard = true;
|
||||||
|
let clipboardWarnLogged = false;
|
||||||
|
export function setPreserveClipboard(v) { preserveClipboard = !!v; }
|
||||||
|
export async function saveClipboard() {
|
||||||
|
if (!page) return;
|
||||||
|
try {
|
||||||
|
await page.evaluate(async () => {
|
||||||
|
try {
|
||||||
|
const items = await navigator.clipboard.read();
|
||||||
|
const saved = [];
|
||||||
|
for (const item of items) {
|
||||||
|
const types = {};
|
||||||
|
for (const t of item.types) types[t] = await item.getType(t);
|
||||||
|
saved.push(types);
|
||||||
|
}
|
||||||
|
window.__webTestSavedClipboard = saved;
|
||||||
|
delete window.__webTestClipboardError;
|
||||||
|
} catch (e) {
|
||||||
|
window.__webTestSavedClipboard = null;
|
||||||
|
window.__webTestClipboardError = e?.name || String(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// page.evaluate itself failed (closed page, navigation in flight) — skip.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export async function restoreClipboard() {
|
||||||
|
if (!page) return;
|
||||||
|
let err = null;
|
||||||
|
try {
|
||||||
|
err = await page.evaluate(async () => {
|
||||||
|
const saved = window.__webTestSavedClipboard;
|
||||||
|
const captured = window.__webTestClipboardError || null;
|
||||||
|
delete window.__webTestSavedClipboard;
|
||||||
|
delete window.__webTestClipboardError;
|
||||||
|
try {
|
||||||
|
if (!saved || saved.length === 0) {
|
||||||
|
// Save failed (e.g. CF_HDROP from Explorer not readable via Clipboard API)
|
||||||
|
// or buffer was empty. Either way, the test's writeText already destroyed
|
||||||
|
// any prior native formats in the OS clipboard, so explicitly clear here
|
||||||
|
// to avoid leaking the test value into the user's clipboard.
|
||||||
|
await navigator.clipboard.writeText('');
|
||||||
|
return captured;
|
||||||
|
}
|
||||||
|
const items = saved.map(types => new ClipboardItem(types));
|
||||||
|
await navigator.clipboard.write(items);
|
||||||
|
return null;
|
||||||
|
} catch (e) {
|
||||||
|
return e?.name || String(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (err && !clipboardWarnLogged) {
|
||||||
|
clipboardWarnLogged = true;
|
||||||
|
console.error(`[web-test] clipboard preserve skipped: ${err} (logged once per session)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Paste `text` via OS clipboard (the only trusted-paste path that 1C respects
|
||||||
|
* for autocomplete and Cyrillic). Wraps the writeText+confirm-key pair in a
|
||||||
|
* narrow save/restore so a user's clipboard survives the test run — the window
|
||||||
|
* between save and restore is microseconds.
|
||||||
|
*
|
||||||
|
* - `confirm` — key (string) or sequence (array) to press after writeText.
|
||||||
|
* Defaults to 'Control+V'. Use ['Control+a', 'Control+v'] for select-all-then-paste,
|
||||||
|
* or 'Shift+F11' for the goto-link dialog.
|
||||||
|
* - `postDelay` — ms to wait between confirm-press and restore, for dialogs
|
||||||
|
* that read clipboard asynchronously (e.g. Shift+F11). Default 0.
|
||||||
|
*/
|
||||||
|
export async function pasteText(text, { confirm = 'Control+V', postDelay = 0 } = {}) {
|
||||||
|
if (!page) return;
|
||||||
|
if (preserveClipboard) await saveClipboard();
|
||||||
|
try {
|
||||||
|
await page.evaluate(`navigator.clipboard.writeText(${JSON.stringify(String(text))})`);
|
||||||
|
if (Array.isArray(confirm)) {
|
||||||
|
for (const key of confirm) await page.keyboard.press(key);
|
||||||
|
} else if (confirm) {
|
||||||
|
await page.keyboard.press(confirm);
|
||||||
|
}
|
||||||
|
if (postDelay) await page.waitForTimeout(postDelay);
|
||||||
|
} finally {
|
||||||
|
if (preserveClipboard) await restoreClipboard();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find the 1C browser extension in Chrome/Edge user profiles.
|
* Find the 1C browser extension in Chrome/Edge user profiles.
|
||||||
* Returns the path to the latest version, or null if not found.
|
* Returns the path to the latest version, or null if not found.
|
||||||
@@ -1137,8 +1229,7 @@ export async function navigateLink(url) {
|
|||||||
const formBefore = await page.evaluate(detectFormScript());
|
const formBefore = await page.evaluate(detectFormScript());
|
||||||
|
|
||||||
// Copy link to clipboard, press Shift+F11 (opens "Go to link" dialog with clipboard content)
|
// Copy link to clipboard, press Shift+F11 (opens "Go to link" dialog with clipboard content)
|
||||||
await page.evaluate(`navigator.clipboard.writeText(${JSON.stringify(link)})`);
|
await pasteText(link, { confirm: 'Shift+F11', postDelay: 200 });
|
||||||
await page.keyboard.press('Shift+F11');
|
|
||||||
await waitForStable();
|
await waitForStable();
|
||||||
|
|
||||||
// Click "Перейти" in the navigation dialog
|
// Click "Перейти" in the navigation dialog
|
||||||
@@ -1868,8 +1959,7 @@ async function advancedSearchInline(formNum, text) {
|
|||||||
await page.click(`[id="${patternId}"]`);
|
await page.click(`[id="${patternId}"]`);
|
||||||
await page.waitForTimeout(200);
|
await page.waitForTimeout(200);
|
||||||
await page.keyboard.press('Control+A');
|
await page.keyboard.press('Control+A');
|
||||||
await page.evaluate(`navigator.clipboard.writeText(${JSON.stringify(String(text))})`);
|
await pasteText(text);
|
||||||
await page.keyboard.press('Control+V');
|
|
||||||
await page.waitForTimeout(300);
|
await page.waitForTimeout(300);
|
||||||
|
|
||||||
// 4. Click "Найти"
|
// 4. Click "Найти"
|
||||||
@@ -1971,8 +2061,7 @@ async function pickFromSelectionForm(selFormNum, fieldName, search, origFormNum)
|
|||||||
await page.click(`[id="${searchInputId}"]`);
|
await page.click(`[id="${searchInputId}"]`);
|
||||||
await page.waitForTimeout(200);
|
await page.waitForTimeout(200);
|
||||||
await page.keyboard.press('Control+A');
|
await page.keyboard.press('Control+A');
|
||||||
await page.evaluate(`navigator.clipboard.writeText(${JSON.stringify(String(searchText))})`);
|
await pasteText(searchText);
|
||||||
await page.keyboard.press('Control+V');
|
|
||||||
await page.waitForTimeout(300);
|
await page.waitForTimeout(300);
|
||||||
await page.keyboard.press('Enter');
|
await page.keyboard.press('Enter');
|
||||||
await waitForStable(selFormNum);
|
await waitForStable(selFormNum);
|
||||||
@@ -2112,8 +2201,7 @@ async function pickFromTypeDialog(formNum, typeName) {
|
|||||||
|
|
||||||
// Paste search text (focus is on "Что искать" field)
|
// Paste search text (focus is on "Что искать" field)
|
||||||
await page.keyboard.press('Control+a');
|
await page.keyboard.press('Control+a');
|
||||||
await page.evaluate(`navigator.clipboard.writeText(${JSON.stringify(typeName)})`);
|
await pasteText(typeName);
|
||||||
await page.keyboard.press('Control+v');
|
|
||||||
await page.waitForTimeout(300);
|
await page.waitForTimeout(300);
|
||||||
|
|
||||||
// Find the "Найти" dialog form number (it's > formNum)
|
// Find the "Найти" dialog form number (it's > formNum)
|
||||||
@@ -2302,8 +2390,7 @@ async function fillReferenceField(selector, fieldName, value, formNum) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 3. Paste text via clipboard (trusted event → triggers real 1C autocomplete)
|
// 3. Paste text via clipboard (trusted event → triggers real 1C autocomplete)
|
||||||
await page.evaluate(`navigator.clipboard.writeText(${JSON.stringify(text)})`);
|
await pasteText(text);
|
||||||
await page.keyboard.press('Control+V');
|
|
||||||
await page.waitForTimeout(2000);
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
// 4. Check editDropDown for autocomplete suggestions
|
// 4. Check editDropDown for autocomplete suggestions
|
||||||
@@ -2518,8 +2605,7 @@ export async function fillFields(fields) {
|
|||||||
await page.click(selector);
|
await page.click(selector);
|
||||||
await page.waitForTimeout(200);
|
await page.waitForTimeout(200);
|
||||||
await page.keyboard.press('Control+A');
|
await page.keyboard.press('Control+A');
|
||||||
await page.evaluate(`navigator.clipboard.writeText(${JSON.stringify(String(fields[r.field]))})`);
|
await pasteText(fields[r.field]);
|
||||||
await page.keyboard.press('Control+V');
|
|
||||||
await page.waitForTimeout(300);
|
await page.waitForTimeout(300);
|
||||||
await page.keyboard.press('Tab');
|
await page.keyboard.press('Tab');
|
||||||
await waitForStable();
|
await waitForStable();
|
||||||
@@ -2539,8 +2625,7 @@ export async function fillFields(fields) {
|
|||||||
await page.click(selector);
|
await page.click(selector);
|
||||||
await page.waitForTimeout(200);
|
await page.waitForTimeout(200);
|
||||||
await page.keyboard.press('Control+A');
|
await page.keyboard.press('Control+A');
|
||||||
await page.evaluate(`navigator.clipboard.writeText(${JSON.stringify(String(fields[r.field]))})`);
|
await pasteText(fields[r.field]);
|
||||||
await page.keyboard.press('Control+V');
|
|
||||||
await page.waitForTimeout(300);
|
await page.waitForTimeout(300);
|
||||||
await page.keyboard.press('Tab');
|
await page.keyboard.press('Tab');
|
||||||
await waitForStable();
|
await waitForStable();
|
||||||
@@ -3818,9 +3903,7 @@ export async function fillTableRow(fields, { tab, add, row, table } = {}) {
|
|||||||
})()`);
|
})()`);
|
||||||
if (selForm === null && inInputAfterDblclick) {
|
if (selForm === null && inInputAfterDblclick) {
|
||||||
// Plain text/numeric field — fill via clipboard paste
|
// Plain text/numeric field — fill via clipboard paste
|
||||||
await page.evaluate(`navigator.clipboard.writeText(${JSON.stringify(info.value)})`);
|
await pasteText(info.value, { confirm: ['Control+a', 'Control+v'] });
|
||||||
await page.keyboard.press('Control+a');
|
|
||||||
await page.keyboard.press('Control+v');
|
|
||||||
await page.waitForTimeout(400);
|
await page.waitForTimeout(400);
|
||||||
// Dismiss EDD autocomplete if it appeared
|
// Dismiss EDD autocomplete if it appeared
|
||||||
const hasEdd = await page.evaluate(`(() => {
|
const hasEdd = await page.evaluate(`(() => {
|
||||||
@@ -4135,9 +4218,7 @@ export async function fillTableRow(fields, { tab, add, row, table } = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await page.evaluate(`navigator.clipboard.writeText(${JSON.stringify(text)})`);
|
await pasteText(text, { confirm: ['Control+a', 'Control+v'] });
|
||||||
await page.keyboard.press('Control+a');
|
|
||||||
await page.keyboard.press('Control+v');
|
|
||||||
await page.waitForTimeout(400);
|
await page.waitForTimeout(400);
|
||||||
await page.keyboard.press('Tab');
|
await page.keyboard.press('Tab');
|
||||||
await page.waitForTimeout(300);
|
await page.waitForTimeout(300);
|
||||||
@@ -4169,8 +4250,7 @@ export async function fillTableRow(fields, { tab, add, row, table } = {}) {
|
|||||||
|
|
||||||
// === Fill this cell: clipboard paste (trusted event) ===
|
// === Fill this cell: clipboard paste (trusted event) ===
|
||||||
await page.keyboard.press('Control+A');
|
await page.keyboard.press('Control+A');
|
||||||
await page.evaluate(`navigator.clipboard.writeText(${JSON.stringify(text)})`);
|
await pasteText(text);
|
||||||
await page.keyboard.press('Control+V');
|
|
||||||
await page.waitForTimeout(1500);
|
await page.waitForTimeout(1500);
|
||||||
|
|
||||||
// Check if paste was rejected (composite-type cell blocks text input until type is selected)
|
// Check if paste was rejected (composite-type cell blocks text input until type is selected)
|
||||||
@@ -4421,9 +4501,7 @@ export async function fillTableRow(fields, { tab, add, row, table } = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await page.evaluate(`navigator.clipboard.writeText(${JSON.stringify(text)})`);
|
await pasteText(text, { confirm: ['Control+a', 'Control+v'] });
|
||||||
await page.keyboard.press('Control+a');
|
|
||||||
await page.keyboard.press('Control+v');
|
|
||||||
await page.waitForTimeout(400);
|
await page.waitForTimeout(400);
|
||||||
await page.keyboard.press('Tab');
|
await page.keyboard.press('Tab');
|
||||||
await page.waitForTimeout(300);
|
await page.waitForTimeout(300);
|
||||||
@@ -4668,8 +4746,7 @@ export async function filterList(text, { field, exact } = {}) {
|
|||||||
await page.click(`[id="${searchId}"]`);
|
await page.click(`[id="${searchId}"]`);
|
||||||
await page.waitForTimeout(200);
|
await page.waitForTimeout(200);
|
||||||
await page.keyboard.press('Control+A');
|
await page.keyboard.press('Control+A');
|
||||||
await page.evaluate(`navigator.clipboard.writeText(${JSON.stringify(String(text))})`);
|
await pasteText(text);
|
||||||
await page.keyboard.press('Control+V');
|
|
||||||
await page.waitForTimeout(300);
|
await page.waitForTimeout(300);
|
||||||
await page.keyboard.press('Enter');
|
await page.keyboard.press('Enter');
|
||||||
await waitForStable(formNum);
|
await waitForStable(formNum);
|
||||||
@@ -4849,8 +4926,7 @@ export async function filterList(text, { field, exact } = {}) {
|
|||||||
await page.waitForTimeout(100);
|
await page.waitForTimeout(100);
|
||||||
await page.keyboard.press('Shift+End');
|
await page.keyboard.press('Shift+End');
|
||||||
await page.waitForTimeout(100);
|
await page.waitForTimeout(100);
|
||||||
await page.evaluate(`navigator.clipboard.writeText(${JSON.stringify(String(text))})`);
|
await pasteText(text);
|
||||||
await page.keyboard.press('Control+V');
|
|
||||||
await page.waitForTimeout(500);
|
await page.waitForTimeout(500);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -4858,8 +4934,7 @@ export async function filterList(text, { field, exact } = {}) {
|
|||||||
await page.click(`[id="${dialogInfo.patternId}"]`);
|
await page.click(`[id="${dialogInfo.patternId}"]`);
|
||||||
await page.waitForTimeout(200);
|
await page.waitForTimeout(200);
|
||||||
await page.keyboard.press('Control+A');
|
await page.keyboard.press('Control+A');
|
||||||
await page.evaluate(`navigator.clipboard.writeText(${JSON.stringify(String(text))})`);
|
await pasteText(text);
|
||||||
await page.keyboard.press('Control+V');
|
|
||||||
await page.waitForTimeout(300);
|
await page.waitForTimeout(300);
|
||||||
|
|
||||||
if (dialogInfo.isRef) {
|
if (dialogInfo.isRef) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
// web-test run v1.14 — CLI runner for 1C web client automation
|
// web-test run v1.16 — CLI runner for 1C web client automation
|
||||||
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
/**
|
/**
|
||||||
* CLI runner for 1C web client automation.
|
* CLI runner for 1C web client automation.
|
||||||
@@ -39,6 +39,14 @@ const flags = {
|
|||||||
};
|
};
|
||||||
const args = rawArgs.filter(a => !a.startsWith('--'));
|
const args = rawArgs.filter(a => !a.startsWith('--'));
|
||||||
|
|
||||||
|
// Clipboard preservation: default ON. Disabled by --no-preserve-clipboard CLI flag
|
||||||
|
// or WEB_TEST_PRESERVE_CLIPBOARD=0 env. cmdTest may further disable via config.
|
||||||
|
// Forwarded to browser.setPreserveClipboard() — narrow save/restore lives around
|
||||||
|
// each writeText+Ctrl+V pair inside pasteText() in browser.mjs.
|
||||||
|
const preserveClipboard = !rawArgs.includes('--no-preserve-clipboard')
|
||||||
|
&& process.env.WEB_TEST_PRESERVE_CLIPBOARD !== '0';
|
||||||
|
browser.setPreserveClipboard(preserveClipboard);
|
||||||
|
|
||||||
function parseExecTimeoutMs(argv) {
|
function parseExecTimeoutMs(argv) {
|
||||||
const DEFAULT_MS = 30 * 60 * 1000;
|
const DEFAULT_MS = 30 * 60 * 1000;
|
||||||
const flagMs = argv.find(a => a.startsWith('--timeout='));
|
const flagMs = argv.find(a => a.startsWith('--timeout='));
|
||||||
@@ -449,6 +457,10 @@ async function cmdTest(rawArgs) {
|
|||||||
if (!tags && config.tags) tags = config.tags;
|
if (!tags && config.tags) tags = config.tags;
|
||||||
opts.timeout = ownArgs.some(a => a.startsWith('--timeout=')) ? opts.timeout : (config.timeout || opts.timeout);
|
opts.timeout = ownArgs.some(a => a.startsWith('--timeout=')) ? opts.timeout : (config.timeout || opts.timeout);
|
||||||
opts.retry = ownArgs.some(a => a.startsWith('--retry=')) ? opts.retry : (config.retries || opts.retry);
|
opts.retry = ownArgs.some(a => a.startsWith('--retry=')) ? opts.retry : (config.retries || opts.retry);
|
||||||
|
// Clipboard preservation: CLI flag wins (already applied at boot), else config can disable.
|
||||||
|
if (config.preserveClipboard === false && !ownArgs.includes('--no-preserve-clipboard')) {
|
||||||
|
browser.setPreserveClipboard(false);
|
||||||
|
}
|
||||||
opts.record = opts.record || !!config.record;
|
opts.record = opts.record || !!config.record;
|
||||||
opts.screenshot = opts.screenshot || config.screenshot || 'on-failure';
|
opts.screenshot = opts.screenshot || config.screenshot || 'on-failure';
|
||||||
if (!['on-failure', 'every-step', 'off'].includes(opts.screenshot)) {
|
if (!['on-failure', 'every-step', 'off'].includes(opts.screenshot)) {
|
||||||
@@ -1225,6 +1237,10 @@ Commands:
|
|||||||
Options for exec:
|
Options for exec:
|
||||||
--no-record Skip video recording (record() becomes no-op)
|
--no-record Skip video recording (record() becomes no-op)
|
||||||
|
|
||||||
|
Global options (any command):
|
||||||
|
--no-preserve-clipboard Don't save/restore OS clipboard around action calls.
|
||||||
|
Default: on (env: WEB_TEST_PRESERVE_CLIPBOARD=0 to disable globally).
|
||||||
|
|
||||||
Options for test:
|
Options for test:
|
||||||
--tags=smoke,crud Filter tests by tags
|
--tags=smoke,crud Filter tests by tags
|
||||||
--grep=pattern Filter tests by name (regex)
|
--grep=pattern Filter tests by name (regex)
|
||||||
|
|||||||
@@ -21,6 +21,12 @@ export default {
|
|||||||
// extension may not load (Playwright limitation). Use only when really needed.
|
// extension may not load (Playwright limitation). Use only when really needed.
|
||||||
timeout: 60000,
|
timeout: 60000,
|
||||||
|
|
||||||
|
// OS clipboard preservation: default `true`. Around every action call the engine
|
||||||
|
// saves the full clipboard contents (any MIME types via `navigator.clipboard.read()`)
|
||||||
|
// and restores them after, so a local user can copy/paste in parallel with a test run.
|
||||||
|
// Set to `false` to disable for this suite. CLI flag `--no-preserve-clipboard` overrides.
|
||||||
|
preserveClipboard: true,
|
||||||
|
|
||||||
// Allure severity policy: inverted map "уровень → теги, попадающие в этот уровень".
|
// Allure severity policy: inverted map "уровень → теги, попадающие в этот уровень".
|
||||||
// Резолв (run.mjs:resolveSeverity):
|
// Резолв (run.mjs:resolveSeverity):
|
||||||
// 1. explicit `export const severity` в тесте — выигрывает всегда;
|
// 1. explicit `export const severity` в тесте — выигрывает всегда;
|
||||||
|
|||||||
Reference in New Issue
Block a user