This commit is contained in:
+71
-17
@@ -1,25 +1,33 @@
|
||||
import { test, expect, type Page } from '@playwright/test';
|
||||
import { openSpaceTab } from './helpers/spaceTabs';
|
||||
|
||||
// ── Schedules (scheduled task CRUD) E2E ───────────────────────────────────────
|
||||
// ── Automations (scheduled task CRUD) E2E ─────────────────────────────────────
|
||||
//
|
||||
// REQUIRES `npm run test:e2e` + a running server (ui/playwright.config.ts boots
|
||||
// the real orchestrator with auth OFF → synthetic 'local' user). This does NOT
|
||||
// run in the sandbox — there is no live server here.
|
||||
//
|
||||
// The Schedules page (page=schedules) is NOT auth-gated (NAV_ITEMS: schedules
|
||||
// requiresAuth:false). The cronForm lib is partially unit-covered; this proves
|
||||
// the create/list/edit UI wiring that has no e2e.
|
||||
// WHERE the page lives: commit e6733e58 retired the top-level Automations tab
|
||||
// and moved automations inside each workspace — `?page=schedules` now falls back
|
||||
// to the spaces view, so the old `page.goto('/ui?page=schedules')` never reached
|
||||
// the page and both tests in this file had been failing since (issue #1338). The
|
||||
// screen itself is unchanged: SpaceDetail renders the SAME SchedulesPage
|
||||
// component under its 自動実行 tab, which lives in the 「その他」 overflow menu
|
||||
// (helpers/spaceTabs.ts). So this spec drives it there instead, and the
|
||||
// selectors below still hold. Neither the workspaces view nor the automations
|
||||
// screen is auth-gated. The cronForm lib is partially unit-covered; this proves
|
||||
// the create/list/edit UI wiring that has no other e2e.
|
||||
//
|
||||
// Selector grounding (read from SchedulesPage.tsx + i18n locales en/ja):
|
||||
// - List: a "New schedule" / "新しいスケジュール" button (list.new) opens the editor.
|
||||
// - List: a "New automation" / "新しい自動実行" button (list.new) opens the editor.
|
||||
// - Editor: a Title input (placeholder "Weekly news roundup" / "週次ニュースまとめ"),
|
||||
// a Prompt textarea (placeholder "Enter the prompt to run" / "実行するプロンプトを入力"),
|
||||
// and a submit button "Create"/"作成" (gated on a non-empty prompt body).
|
||||
// - Each schedule renders as a list <button> showing its title.
|
||||
// Because these controls are i18n-text driven (no test-ids) and the runner's
|
||||
// browser locale decides en vs ja, text selectors below use bilingual regexes.
|
||||
// Backend state is verified through GET /api/scheduled-tasks — the same endpoint
|
||||
// the page reads.
|
||||
// Backend state is verified through GET /api/scheduled-tasks?spaceId=… — the
|
||||
// same endpoint the space-scoped page reads.
|
||||
|
||||
const BENIGN_PATHS = new Set([
|
||||
'/api/auth/me',
|
||||
@@ -30,6 +38,11 @@ const BENIGN_PATHS = new Set([
|
||||
'/api/ssh/connections',
|
||||
]);
|
||||
const BENIGN_ASSET = /\.(ico|png|svg|map|webmanifest|json)$/i;
|
||||
// The workspace opens on its chat tab before we reach 自動実行; those two probes
|
||||
// 404 on a workspace with no console session / browser session (same allowlist
|
||||
// as spaces.spec.ts and calendar.spec.ts).
|
||||
const BENIGN_PATH_RE =
|
||||
/\/api\/local\/tasks\/\d+\/console\/status$|\/api\/local\/browser\/sessions\/task-session\/\d+$/;
|
||||
|
||||
function trackFatalErrors(page: Page): string[] {
|
||||
const fatalErrors: string[] = [];
|
||||
@@ -37,7 +50,7 @@ function trackFatalErrors(page: Page): string[] {
|
||||
page.on('response', (res) => {
|
||||
if (res.status() >= 400) {
|
||||
const { pathname } = new URL(res.url());
|
||||
if (!BENIGN_ASSET.test(pathname) && !BENIGN_PATHS.has(pathname)) {
|
||||
if (!BENIGN_ASSET.test(pathname) && !BENIGN_PATHS.has(pathname) && !BENIGN_PATH_RE.test(pathname)) {
|
||||
fatalErrors.push(`HTTP ${res.status()} ${res.url()}`);
|
||||
}
|
||||
}
|
||||
@@ -45,23 +58,63 @@ function trackFatalErrors(page: Page): string[] {
|
||||
return fatalErrors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a case workspace and open it. Returns its id so the spec can scope the
|
||||
* backend reads to it (mirrors the same helper in calendar.spec.ts).
|
||||
*/
|
||||
async function createAndOpenCaseSpace(page: Page, label: string): Promise<string> {
|
||||
await page.goto('/ui');
|
||||
await page.getByTestId('nav-spaces').click();
|
||||
const rail = page.getByTestId('space-rail');
|
||||
await expect(rail).toBeVisible();
|
||||
|
||||
await page.getByTestId('create-space-btn').click();
|
||||
const titleInput = page.getByTestId('space-title-input');
|
||||
await expect(titleInput).toBeVisible();
|
||||
const caseTitle = `${label}-${Date.now()}`;
|
||||
await titleInput.fill(caseTitle);
|
||||
await page.getByTestId('space-form-submit').click();
|
||||
await expect(titleInput).toHaveCount(0);
|
||||
|
||||
const caseRow = rail
|
||||
.locator('[data-testid="space-row"][data-space-kind="case"]')
|
||||
.filter({ hasText: caseTitle });
|
||||
await expect(caseRow).toBeVisible();
|
||||
const spaceId = await caseRow.getAttribute('data-space-id');
|
||||
expect(spaceId, 'created space id').toBeTruthy();
|
||||
await caseRow.click();
|
||||
await expect(page.getByTestId('space-detail')).toBeVisible();
|
||||
return spaceId as string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reopen an existing workspace's 自動実行 tab from a cold load. The open tab is
|
||||
* component state, not a URL param, so the reload lands on 会話 and the overflow
|
||||
* menu has to be walked again.
|
||||
*/
|
||||
async function reopenSpaceSchedules(page: Page, spaceId: string): Promise<void> {
|
||||
await page.goto(`/ui?space=${encodeURIComponent(spaceId)}`);
|
||||
await expect(page.getByTestId('space-detail')).toBeVisible();
|
||||
await openSpaceTab(page, 'schedules');
|
||||
}
|
||||
|
||||
// Bilingual matchers (the runner's browser locale picks en or ja; the harness
|
||||
// pins no localStorage language, so navigator decides).
|
||||
const NEW_SCHEDULE = /New schedule|新しいスケジュール/;
|
||||
const NEW_AUTOMATION = /New automation|新しい自動実行/;
|
||||
const SUBMIT_CREATE = /^(Create|作成)$/;
|
||||
const TITLE_PLACEHOLDER = /Weekly news roundup|週次ニュースまとめ/;
|
||||
const PROMPT_PLACEHOLDER = /Enter the prompt to run|実行するプロンプトを入力/;
|
||||
|
||||
const scheduleTitle = `e2e-schedule-${Date.now()}`;
|
||||
|
||||
test('create a scheduled task via the UI, then see it listed and persisted', async ({ page }) => {
|
||||
test('create an automation via the UI, then see it listed and persisted', async ({ page }) => {
|
||||
const fatalErrors = trackFatalErrors(page);
|
||||
|
||||
await page.goto('/ui?page=schedules');
|
||||
await expect(page).toHaveURL(/[?&]page=schedules(&|$)/);
|
||||
const spaceId = await createAndOpenCaseSpace(page, 'E2E自動実行');
|
||||
await openSpaceTab(page, 'schedules');
|
||||
|
||||
// Open the editor.
|
||||
await page.getByRole('button', { name: NEW_SCHEDULE }).first().click();
|
||||
await page.getByRole('button', { name: NEW_AUTOMATION }).first().click();
|
||||
|
||||
// Fill the editor: title + prompt (the submit button is gated on a non-empty
|
||||
// prompt body). Default scheduleType is 'daily', so no extra cron input needed.
|
||||
@@ -80,7 +133,7 @@ test('create a scheduled task via the UI, then see it listed and persisted', asy
|
||||
let createdId = 0;
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const res = await page.request.get('/api/scheduled-tasks');
|
||||
const res = await page.request.get(`/api/scheduled-tasks?spaceId=${encodeURIComponent(spaceId)}`);
|
||||
if (!res.ok()) return [];
|
||||
const data = (await res.json()) as Array<{ id: number; title: string; cronExpression?: string }> | { tasks?: Array<{ id: number; title: string }> };
|
||||
const list = Array.isArray(data) ? data : (data.tasks ?? []);
|
||||
@@ -101,7 +154,7 @@ test('create a scheduled task via the UI, then see it listed and persisted', asy
|
||||
expect(patched.ok(), 'edit schedule title').toBeTruthy();
|
||||
|
||||
// The list reflects the edit after a fresh load (refetches /api/scheduled-tasks).
|
||||
await page.goto('/ui?page=schedules');
|
||||
await reopenSpaceSchedules(page, spaceId);
|
||||
await expect(page.getByRole('button', { name: new RegExp(editedTitle) })).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
expect(fatalErrors, `fatal errors:\n${fatalErrors.join('\n')}`).toEqual([]);
|
||||
@@ -112,8 +165,9 @@ test('create a scheduled task via the UI, then see it listed and persisted', asy
|
||||
test('the create button stays disabled until a prompt is entered', async ({ page }) => {
|
||||
const fatalErrors = trackFatalErrors(page);
|
||||
|
||||
await page.goto('/ui?page=schedules');
|
||||
await page.getByRole('button', { name: NEW_SCHEDULE }).first().click();
|
||||
await createAndOpenCaseSpace(page, 'E2E自動実行ガード');
|
||||
await openSpaceTab(page, 'schedules');
|
||||
await page.getByRole('button', { name: NEW_AUTOMATION }).first().click();
|
||||
|
||||
// With only a title (no prompt body) the submit is disabled.
|
||||
await page.getByPlaceholder(TITLE_PLACEHOLDER).fill('no-prompt-schedule');
|
||||
|
||||
Reference in New Issue
Block a user