import assert from 'node:assert/strict'; import { existsSync } from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { createServer } from 'vite'; import react from '@vitejs/plugin-react'; import { chromium } from 'playwright'; // Real query/mutation/form hooks and the production schedule modal. Only HTTP, // current workspace, toast output and translation are controlled boundaries. const root = fileURLToPath(new URL('../', import.meta.url)); const fixture = ` import React,{useState} from 'react'; import {createRoot} from 'react-dom/client'; import {WorkspaceTestContext} from '/src/contexts/useWorkspace.js'; import {useBackupSchedules} from '/src/hooks/useBackupSchedules.js'; import AddScheduleModal from '/src/components/backups/AddScheduleModal.jsx'; window.calls=[];window.messages=[];window.workspace='one'; window.toast={error:value=>window.messages.push(['error',value]),success:value=>window.messages.push(['success',value])}; window.rows={one:[{id:1,name:'Daily',enabled:true,next_run_at:'2026-09-06T02:00:00-04:00',timezone:'America/New_York'}],two:[{id:2,name:'Other workspace',enabled:true}]}; window.testApi={ getBackupSchedules:()=>{window.calls.push(['get',window.workspace]);const result={schedules:window.rows[window.workspace].map(row=>({...row})),timezone:'America/New_York'};return window.holdRead?new Promise(resolve=>window.finishRead=()=>resolve(result)):Promise.resolve(result)}, addBackupSchedule:(...args)=>{window.calls.push(['create',...args]);return new Promise((resolve,reject)=>{window.finishCreate=()=>{window.rows.one.push({id:3,name:args[0],enabled:true});resolve({id:3})};window.failCreate=()=>reject(Object.assign(new Error('Please fix the field'),{fieldErrors:{name:'Already exists'}}))})}, updateBackupSchedule:(id,body)=>{window.calls.push(['toggle',id,body]);window.rows[window.workspace]=window.rows[window.workspace].map(row=>row.id===id?{...row,...body}:row);return Promise.resolve({})}, removeBackupSchedule:id=>{window.calls.push(['remove',id]);window.rows[window.workspace]=window.rows[window.workspace].filter(row=>row.id!==id);return Promise.resolve({})} }; function Screen(){const store=useBackupSchedules();const [open,setOpen]=useState(true);return <> {JSON.stringify(store.schedules)} setOpen(false)} onCreate={store.create} onCreated={()=>setOpen(false)} remoteEnabled timezone={store.timezone}/> } function App(){const [workspace,setWorkspace]=useState('one');window.changeWorkspace=()=>{window.workspace='two';setWorkspace('two')};return } createRoot(document.getElementById('root')).render(); `; const server = await createServer({ root, configFile: false, logLevel: 'error', resolve: { alias: { '@': path.join(root, 'src') } }, cacheDir: path.join(root, 'node_modules/.vite-backups-regression'), optimizeDeps: { entries: [] }, server: { host: '127.0.0.1', port: 0 }, plugins: [{ name: 'backup-regression-fixtures', enforce: 'pre', resolveId(id) { if (id === 'react-i18next') return '\0test-i18n'; if (id.endsWith('/backups-fixture.jsx')) return path.join(root, 'backups-fixture.jsx'); }, load(id) { const normalized = id.replaceAll('\\', '/'); if (id === '\0test-i18n') return `const t=(key,fallback)=>typeof fallback==='string'?fallback:key;export const useTranslation=()=>({t});`; if (normalized.endsWith('/src/contexts/useWorkspace.js')) return `import {createContext,useContext} from 'react';export const WorkspaceTestContext=createContext({});export const useWorkspace=()=>useContext(WorkspaceTestContext);`; if (normalized.endsWith('/src/contexts/useToast.js')) return `export const useToast=()=>window.toast;`; if (normalized.endsWith('/src/services/api.js') || normalized.endsWith('/src/services/api/index.js')) return `export default new Proxy({}, {get:(_,key)=>(...args)=>window.testApi[key](...args)});`; if (normalized.endsWith('/backups-fixture.jsx')) return fixture; }, configureServer(vite) { vite.middlewares.use('/backups-check', async (_req, res) => { res.setHeader('Content-Type', 'text/html'); res.end(await vite.transformIndexHtml('/backups-check', '
')); }); }, }, react()], }); const executablePath = [process.env.CHROME_PATH, chromium.executablePath(), 'C:/Program Files/Google/Chrome/Application/chrome.exe', 'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe'].find(candidate => candidate && existsSync(candidate)); let browser; try { await server.listen(); browser = await chromium.launch({ ...(executablePath ? { executablePath } : {}), headless: true }); const page = await browser.newPage(); const errors = []; page.on('pageerror', error => errors.push(error.message)); await page.goto(`http://127.0.0.1:${server.httpServer.address().port}/backups-check`); await page.getByLabel('Schedule name', { exact: false }).fill('Daily 2'); await page.getByLabel('Service name', { exact: false }).fill('my-app'); assert.equal(await page.locator('.form-field__hint').textContent(), 'America/New_York'); await page.evaluate(() => { const form = document.querySelector('[data-walkthrough="backup-schedule-form"]'); form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true })); form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true })); }); await page.waitForFunction(() => window.calls.filter(([type]) => type === 'create').length === 1); assert.equal(await page.locator('[data-walkthrough="backup-schedule-submit"]').isDisabled(), true); await page.evaluate(() => window.failCreate()); await page.getByText('Already exists', { exact: true }).waitFor(); assert.equal(await page.getByLabel('Schedule name', { exact: false }).inputValue(), 'Daily 2'); await page.getByLabel('Schedule name', { exact: false }).fill('Unique schedule'); await page.locator('[data-walkthrough="backup-schedule-submit"]').click(); await page.waitForFunction(() => window.calls.filter(([type]) => type === 'create').length === 2); await page.evaluate(() => window.finishCreate()); await page.locator('[data-walkthrough="backup-schedule-form"]').waitFor({ state: 'hidden' }); await page.waitForFunction(() => document.querySelector('#rows').textContent.includes('Unique schedule')); console.log('PASS duplicate-submit guard, inline server errors, preserved input, retry and creation cache invalidation'); await page.locator('#toggle').click(); await page.waitForFunction(() => JSON.parse(document.querySelector('#rows').textContent)[0].enabled === false); await page.locator('#remove').click(); await page.waitForFunction(() => JSON.parse(document.querySelector('#rows').textContent).length === 1); assert.equal(await page.evaluate(() => window.messages.filter(([type]) => type === 'success').length), 2); await page.evaluate(() => { window.holdRead = true; window.changeWorkspace(); }); await page.waitForFunction(() => Boolean(window.finishRead)); assert.deepEqual(JSON.parse(await page.locator('#rows').textContent()), []); await page.evaluate(() => window.finishRead()); await page.waitForFunction(() => document.querySelector('#rows').textContent.includes('Other workspace')); assert.deepEqual(errors, []); console.log('PASS toggle/delete query invalidation and workspace isolation while a new workspace request is pending'); } finally { await browser?.close(); await server.close(); }