tests: in-run progress strip + cache-bust runner.js across files
* runner.js: shows a 30px fixed-top progress strip during runs (label "test x/total:", current suite/test name with ellipsis on the left, fixed 50%-wide progress fill on the right that turns danger-red on the first failure). The DOM node is created in loader.js so it appears immediately on shortcut press; runner.js only updates it. The fill width is fixed at flex:0 0 50% so it doesn't jiggle as the test name changes length. Suites now live on window._testSuites as a defensive fallback so two runner instances stay in sync. * Cache invalidation: every test file's `from './runner.js'` is now `from './runner.js?v=2'`, and loader.js imports the runner with the same RUNNER_V suffix. Same URL → one module instance → suite registration and runAll see the same array. Bumping RUNNER_V (in loader.js + every test file in lock-step) is the way to force a re-fetch when runner.js changes. * loader.js: ensureProgressBar() draws the strip via setProperty(..., 'important') with z-index 2147483647 attached to documentElement so nothing in user CSS, scanline overlay (z 9999) or modals can occlude it. Auto-run path (?test=1) also pre-renders the strip. chore: bump version to 0.9.1 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
f6cf7b4358
commit
fe3399a44b
44 files changed
+132
-48
No files matched your search
+1
-1
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
|
||||
define('APP_NAME', 'hyprcrd');
|
||||
define('APP_VERSION', '0.9.0');
|
||||
define('APP_VERSION', '0.9.1');
|
||||
|
||||
// Database path — writable directory, outside webroot
|
||||
define('DB_PATH', __DIR__ . '/data/hyprcrd.db');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect, csrf } from './runner.js';
|
||||
import { describe, it, expect, csrf } from './runner.js?v=2';
|
||||
|
||||
describe('API: Edge Cases', () => {
|
||||
it('create card with special characters in title', async () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect, csrf } from './runner.js';
|
||||
import { describe, it, expect, csrf } from './runner.js?v=2';
|
||||
|
||||
describe('API: Cards CRUD', () => {
|
||||
it('GET /api/cards returns array', async () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, afterEach, expect, $, $$, click, sleep, closeAll, csrf } from './runner.js';
|
||||
import { describe, it, afterEach, expect, $, $$, click, sleep, closeAll, csrf } from './runner.js?v=2';
|
||||
|
||||
describe('Board Rendering', () => {
|
||||
it('renders card elements', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, afterEach, expect, $, $$, click, sleep, closeAll, csrf } from './runner.js';
|
||||
import { describe, it, afterEach, expect, $, $$, click, sleep, closeAll, csrf } from './runner.js?v=2';
|
||||
|
||||
describe('Bucket Context Menu', () => {
|
||||
afterEach(async () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, afterEach, expect, $, $$, click, sleep, closeAll, csrf } from './runner.js';
|
||||
import { describe, it, afterEach, expect, $, $$, click, sleep, closeAll, csrf } from './runner.js?v=2';
|
||||
import { state } from '../../js/modules/state.js';
|
||||
|
||||
describe('Bucket Bar', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, afterEach, expect, $, $$, click, sleep, closeAll } from './runner.js';
|
||||
import { describe, it, afterEach, expect, $, $$, click, sleep, closeAll } from './runner.js?v=2';
|
||||
|
||||
describe('Calendar: Day Click', () => {
|
||||
afterEach(() => closeAll());
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, afterEach, expect, $, click, sleep, closeAll } from './runner.js';
|
||||
import { describe, it, afterEach, expect, $, click, sleep, closeAll } from './runner.js?v=2';
|
||||
|
||||
describe('Calendar', () => {
|
||||
afterEach(() => closeAll());
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, beforeEach, afterEach, expect, $, click, sleep, closeAll } from './runner.js';
|
||||
import { describe, it, beforeEach, afterEach, expect, $, click, sleep, closeAll } from './runner.js?v=2';
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, afterEach, expect, $, $$, click, rightClick, sleep, closeAll, csrf } from './runner.js';
|
||||
import { describe, it, afterEach, expect, $, $$, click, rightClick, sleep, closeAll, csrf } from './runner.js?v=2';
|
||||
import { state } from '../../js/modules/state.js';
|
||||
|
||||
describe('Card: Tag Operations', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, afterEach, expect, $, $$, click, rightClick, sleep, csrf } from './runner.js';
|
||||
import { describe, it, afterEach, expect, $, $$, click, rightClick, sleep, csrf } from './runner.js?v=2';
|
||||
|
||||
describe('Context Menu', () => {
|
||||
afterEach(async () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, afterEach, expect, $, $$, click, sleep, closeAll } from './runner.js';
|
||||
import { describe, it, afterEach, expect, $, $$, click, sleep, closeAll } from './runner.js?v=2';
|
||||
import { showDialog } from '../../js/modules/dialog.js';
|
||||
|
||||
describe('Datepicker: Rendering', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, afterEach, expect, $, click, pressKey, sleep, closeAll, csrf } from './runner.js';
|
||||
import { describe, it, afterEach, expect, $, click, pressKey, sleep, closeAll, csrf } from './runner.js?v=2';
|
||||
|
||||
describe('Detail: Done Toggle', () => {
|
||||
afterEach(() => closeAll());
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, afterEach, expect, $, click, pressKey, sleep, closeAll } from './runner.js';
|
||||
import { describe, it, afterEach, expect, $, click, pressKey, sleep, closeAll } from './runner.js?v=2';
|
||||
|
||||
describe('Detail View', () => {
|
||||
afterEach(() => closeAll());
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, afterEach, expect, $, click, type, sleep, closeAll, csrf } from './runner.js';
|
||||
import { describe, it, afterEach, expect, $, click, type, sleep, closeAll, csrf } from './runner.js?v=2';
|
||||
|
||||
describe('Editor: Color Picker', () => {
|
||||
afterEach(() => closeAll());
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, afterEach, expect, $, click, type, sleep, closeAll, csrf } from './runner.js';
|
||||
import { describe, it, afterEach, expect, $, click, type, sleep, closeAll, csrf } from './runner.js?v=2';
|
||||
|
||||
describe('Editor: Empty Title Prevention', () => {
|
||||
afterEach(() => closeAll());
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, afterEach, expect, $, click, sleep, closeAll } from './runner.js';
|
||||
import { describe, it, afterEach, expect, $, click, sleep, closeAll } from './runner.js?v=2';
|
||||
|
||||
// Helper: set selection range in textarea
|
||||
function selectRange(textarea, start, end) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, afterEach, expect, $, click, pressKey, sleep, closeAll, csrf } from './runner.js';
|
||||
import { describe, it, afterEach, expect, $, click, pressKey, sleep, closeAll, csrf } from './runner.js?v=2';
|
||||
|
||||
describe('Editor: Structure', () => {
|
||||
it('has all elements', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect, csrf } from './runner.js';
|
||||
import { describe, it, expect, csrf } from './runner.js?v=2';
|
||||
|
||||
describe('Export: JSON ZIP', () => {
|
||||
it('export endpoint returns ZIP', async () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, afterEach, expect, $, $$, click, sleep, closeAll, csrf } from './runner.js';
|
||||
import { describe, it, afterEach, expect, $, $$, click, sleep, closeAll, csrf } from './runner.js?v=2';
|
||||
import { state } from '../../js/modules/state.js';
|
||||
|
||||
describe('Favorites Bucket', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, afterEach, expect, $, $$, click, sleep, closeAll, csrf } from './runner.js';
|
||||
import { describe, it, afterEach, expect, $, $$, click, sleep, closeAll, csrf } from './runner.js?v=2';
|
||||
import { state, parseTags, sortedCards } from '../../js/modules/state.js';
|
||||
|
||||
describe('Filtering: Tag AND Logic', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, afterEach, expect, $, click, sleep, closeAll } from './runner.js';
|
||||
import { describe, it, afterEach, expect, $, click, sleep, closeAll } from './runner.js?v=2';
|
||||
|
||||
describe('Gallery', () => {
|
||||
afterEach(() => closeAll());
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, afterEach, expect, $, $$, click, pressKey, sleep, closeAll } from './runner.js';
|
||||
import { describe, it, afterEach, expect, $, $$, click, pressKey, sleep, closeAll } from './runner.js?v=2';
|
||||
|
||||
describe('Help', () => {
|
||||
afterEach(() => closeAll());
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, afterEach, expect, $, click, pressKey, sleep, closeAll, csrf } from './runner.js';
|
||||
import { describe, it, afterEach, expect, $, click, pressKey, sleep, closeAll, csrf } from './runner.js?v=2';
|
||||
|
||||
describe('Image Fullscreen Viewer', () => {
|
||||
afterEach(async () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, afterEach, expect, $, click, sleep, closeAll } from './runner.js';
|
||||
import { describe, it, afterEach, expect, $, click, sleep, closeAll } from './runner.js?v=2';
|
||||
|
||||
function enableLines() {
|
||||
const gutter = $('#editor-gutter');
|
||||
|
||||
@@ -16,23 +16,62 @@ const V = '?v=' + Date.now();
|
||||
|
||||
// Auto-load if ?test in query string
|
||||
if (new URLSearchParams(location.search).has('test')) {
|
||||
ensureProgressBar();
|
||||
import('./tests.js' + V);
|
||||
}
|
||||
|
||||
// Keyboard shortcut: Ctrl+Shift+Alt+T → open runner.
|
||||
// Import runner.js *without* the cache-busting query so we get the same
|
||||
// module instance that the test files register their suites into. With a
|
||||
// `?v=` suffix the browser treats it as a distinct module and we'd open
|
||||
// a fresh runner whose `suites` array is empty (→ "0/0 passed").
|
||||
//
|
||||
// runner.js is imported with the SAME `?v=2` query string the test
|
||||
// files use (see RUNNER_V below). Same URL → same module instance, so
|
||||
// suite registration in test files and runAll in the loader run against
|
||||
// the same `suites` array. Bump RUNNER_V whenever runner.js changes
|
||||
// in a way that needs cache invalidation across users.
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.ctrlKey && e.shiftKey && e.altKey && (e.key === 'T' || e.key === 't')) {
|
||||
e.preventDefault();
|
||||
ensureProgressBar();
|
||||
import('./tests.js' + V).then(() =>
|
||||
import('./runner.js').then(m => m.open?.())
|
||||
import('./runner.js' + RUNNER_V).then(m => m.open?.())
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Must match the `?v=…` suffix used in test files' `from './runner.js?v=…'`
|
||||
// imports — bump both in lock-step to invalidate the runner.js cache.
|
||||
const RUNNER_V = '?v=2';
|
||||
|
||||
function ensureProgressBar() {
|
||||
if (document.getElementById('test-progress')) return;
|
||||
const bar = document.createElement('div');
|
||||
bar.id = 'test-progress';
|
||||
const s = bar.style;
|
||||
s.setProperty('position', 'fixed', 'important');
|
||||
s.setProperty('top', '0', 'important');
|
||||
s.setProperty('left', '0', 'important');
|
||||
s.setProperty('right', '0', 'important');
|
||||
s.setProperty('height', '30px', 'important');
|
||||
s.setProperty('z-index', '2147483647', 'important');
|
||||
s.setProperty('background', '#000', 'important');
|
||||
s.setProperty('color', '#cbd5e1', 'important');
|
||||
s.setProperty('font-family', 'monospace', 'important');
|
||||
s.setProperty('font-size', '11px', 'important');
|
||||
s.setProperty('display', 'flex', 'important');
|
||||
s.setProperty('align-items', 'center', 'important');
|
||||
s.setProperty('gap', '10px', 'important');
|
||||
s.setProperty('padding', '0 12px', 'important');
|
||||
s.setProperty('border-bottom', '1px solid #334155', 'important');
|
||||
s.setProperty('pointer-events', 'none', 'important');
|
||||
bar.innerHTML = `
|
||||
<span id="test-progress-label" style="white-space:nowrap;color:#38bdf8;font-weight:bold;flex-shrink:0;">tests starting…</span>
|
||||
<span id="test-progress-name" style="flex:1 1 0;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#94a3b8;"></span>
|
||||
<div style="position:relative;flex:0 0 50%;height:10px;background:#1e293b;border:1px solid #334155;">
|
||||
<div id="test-progress-fill" style="position:absolute;left:0;top:0;bottom:0;width:0%;background:#38bdf8;transition:width 0.15s linear;"></div>
|
||||
</div>
|
||||
`;
|
||||
document.documentElement.appendChild(bar);
|
||||
}
|
||||
|
||||
hyprcrd?.registerPlugin?.({
|
||||
name: 'tests',
|
||||
version: '1.0.0',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect } from './runner.js';
|
||||
import { describe, it, expect } from './runner.js?v=2';
|
||||
import { renderMd } from '../../js/modules/markdown.js';
|
||||
|
||||
describe('Markdown: Blank Lines', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect } from './runner.js';
|
||||
import { describe, it, expect } from './runner.js?v=2';
|
||||
import { renderMd, formatTodoItems, stripMd } from '../../js/modules/markdown.js';
|
||||
import { state } from '../../js/modules/state.js';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, afterEach, expect, $, sleep, closeAll } from './runner.js';
|
||||
import { describe, it, afterEach, expect, $, sleep, closeAll } from './runner.js?v=2';
|
||||
import { state } from '../../js/modules/state.js';
|
||||
|
||||
describe('Multi-Select', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect, $ } from './runner.js';
|
||||
import { describe, it, expect, $ } from './runner.js?v=2';
|
||||
|
||||
describe('Notification Banner', () => {
|
||||
it('banner element exists', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, afterEach, expect, $, type, pressKey, sleep, closeAll, csrf } from './runner.js';
|
||||
import { describe, it, afterEach, expect, $, type, pressKey, sleep, closeAll, csrf } from './runner.js?v=2';
|
||||
|
||||
describe('Quick Capture: Open/Close', () => {
|
||||
afterEach(async () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect, csrf } from './runner.js';
|
||||
import { describe, it, expect, csrf } from './runner.js?v=2';
|
||||
|
||||
describe('Relay: Push API', () => {
|
||||
it('push without config returns error', async () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect } from './runner.js';
|
||||
import { describe, it, expect } from './runner.js?v=2';
|
||||
import { state } from '../../js/modules/state.js';
|
||||
|
||||
// RRULE is set by CalDAV sync, not via PATCH. Test the data model and existing cards.
|
||||
|
||||
@@ -7,7 +7,13 @@
|
||||
import { getCsrf } from '../../js/modules/util.js';
|
||||
import { stopBackgroundSync, startBackgroundSync } from '../../js/modules/caldav.js';
|
||||
|
||||
const suites = [];
|
||||
// Suites live on window so two runner.js instances (one cached and loaded
|
||||
// by test files via bare imports, one fresh and loaded by loader.js with
|
||||
// ?v=) can share registration. The cached runner provides describe/it/
|
||||
// expect at module-load time; the fresh runner runs `runAll` against the
|
||||
// shared array. Without this we'd see "0/0 passed" whenever the cached
|
||||
// runner is older than the loader.
|
||||
const suites = (window._testSuites = window._testSuites || []);
|
||||
let currentSuite = null;
|
||||
|
||||
// ── Public API ──
|
||||
@@ -200,11 +206,48 @@ async function restoreBackup() {
|
||||
|
||||
// ── Runner ──
|
||||
|
||||
// Fixed-top progress strip shown while tests are running. The main test
|
||||
// panel is hidden during the run so the suite can interact with the UI;
|
||||
// this strip gives the user a way to see how far along the run is.
|
||||
//
|
||||
// loader.js creates the DOM node when the user presses the shortcut so
|
||||
// it appears immediately (loader.js is cache-busted per page load via
|
||||
// the plugin manager's ?v=time()). The runner — which lives at a stable
|
||||
// URL so suite registration is consistent — only updates the existing
|
||||
// node. If runner.js is cached and stale, the placeholder still shows
|
||||
// up so the user can tell something happened.
|
||||
function getProgressBar() {
|
||||
return document.getElementById('test-progress');
|
||||
}
|
||||
function updateProgress(current, total, name, failed) {
|
||||
const bar = getProgressBar();
|
||||
if (!bar) return;
|
||||
const label = bar.querySelector('#test-progress-label');
|
||||
const nameEl = bar.querySelector('#test-progress-name');
|
||||
const fill = bar.querySelector('#test-progress-fill');
|
||||
if (label) label.textContent = `test ${current}/${total}:`;
|
||||
if (nameEl) nameEl.textContent = name;
|
||||
if (fill) {
|
||||
fill.style.width = (current / total * 100) + '%';
|
||||
if (failed) fill.style.background = '#fb7185';
|
||||
}
|
||||
}
|
||||
function hideProgress() {
|
||||
getProgressBar()?.remove();
|
||||
}
|
||||
|
||||
async function runAll() {
|
||||
const results = [];
|
||||
const total = suites.reduce((n, s) => n + s.tests.length, 0);
|
||||
// Initial label before any test has started
|
||||
updateProgress(0, total, 'starting…', false);
|
||||
let done = 0;
|
||||
let anyFailed = false;
|
||||
for (const suite of suites) {
|
||||
const suiteResult = { name: suite.name, tests: [] };
|
||||
for (const test of suite.tests) {
|
||||
done++;
|
||||
updateProgress(done, total, `${suite.name} — ${test.name}`, anyFailed);
|
||||
const entry = { name: test.name, status: 'pass', error: null, time: 0 };
|
||||
const t0 = performance.now();
|
||||
try {
|
||||
@@ -214,6 +257,7 @@ async function runAll() {
|
||||
} catch (err) {
|
||||
entry.status = 'fail';
|
||||
entry.error = err.message;
|
||||
anyFailed = true;
|
||||
// Always run afterEach on failure too
|
||||
try { if (suite.afterEach) await suite.afterEach(); } catch {}
|
||||
}
|
||||
@@ -222,6 +266,7 @@ async function runAll() {
|
||||
}
|
||||
results.push(suiteResult);
|
||||
}
|
||||
hideProgress();
|
||||
return results;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, afterEach, expect, $, click, type, pressKey, sleep, closeAll, csrf } from './runner.js';
|
||||
import { describe, it, afterEach, expect, $, click, type, pressKey, sleep, closeAll, csrf } from './runner.js?v=2';
|
||||
|
||||
describe('Search: Tag + Text Combined', () => {
|
||||
afterEach(() => closeAll());
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, afterEach, expect, $, click, type, pressKey, sleep, closeAll } from './runner.js';
|
||||
import { describe, it, afterEach, expect, $, click, type, pressKey, sleep, closeAll } from './runner.js?v=2';
|
||||
|
||||
describe('Search', () => {
|
||||
afterEach(() => closeAll());
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, afterEach, expect, $, $$, click, closeAll, sleep } from './runner.js';
|
||||
import { describe, it, afterEach, expect, $, $$, click, closeAll, sleep } from './runner.js?v=2';
|
||||
|
||||
describe('Settings: Open/Close', () => {
|
||||
afterEach(() => closeAll());
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect, $ } from './runner.js';
|
||||
import { describe, it, expect, $ } from './runner.js?v=2';
|
||||
|
||||
describe('App Shell', () => {
|
||||
it('header with logo', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, afterEach, expect, $, click, sleep, closeAll } from './runner.js';
|
||||
import { describe, it, afterEach, expect, $, click, sleep, closeAll } from './runner.js?v=2';
|
||||
|
||||
// Helper: simulate typing a snippet trigger in the editor body
|
||||
function typeSnippet(bodyEl, trigger) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect, csrf } from './runner.js';
|
||||
import { describe, it, expect, csrf } from './runner.js?v=2';
|
||||
import { state, sortedCards } from '../../js/modules/state.js';
|
||||
|
||||
describe('Sorting: All Modes', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect } from './runner.js';
|
||||
import { describe, it, expect } from './runner.js?v=2';
|
||||
import { state, parseTags, sortedCards, getChildren, getGroupCards, getRecent, markRecent, getFavorites, toggleFavorite, isFavorite } from '../../js/modules/state.js';
|
||||
|
||||
describe('State: parseTags', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, afterEach, expect, $, $$, click, closeAll, sleep } from './runner.js';
|
||||
import { describe, it, afterEach, expect, $, $$, click, closeAll, sleep } from './runner.js?v=2';
|
||||
|
||||
describe('Theme', () => {
|
||||
it('CSS custom properties set', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, afterEach, expect, $, click, sleep, closeAll } from './runner.js';
|
||||
import { describe, it, afterEach, expect, $, click, sleep, closeAll } from './runner.js?v=2';
|
||||
import { state } from '../../js/modules/state.js';
|
||||
|
||||
describe('Toolbar: Filter Toggles', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, afterEach, expect, $, $$, click, type, rightClick, pressKey, sleep, closeAll, csrf } from './runner.js';
|
||||
import { describe, it, afterEach, expect, $, $$, click, type, rightClick, pressKey, sleep, closeAll, csrf } from './runner.js?v=2';
|
||||
|
||||
function ctrlZ() {
|
||||
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'z', ctrlKey: true, bubbles: true }));
|
||||
|
||||
Reference in new issue
Block a user