Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions e2e/oauth/oauth-overlay.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,9 @@ test.describe("OAuth Overlay", () => {
await mainGrid.waitFor({ state: "visible", timeout: 10000 });
await mainGrid.focus();

await page.waitForTimeout(100); // Give time for focus to settle
await page.waitForFunction(
() => document.activeElement?.tagName !== "BODY",
);

// Verify something is focused (not body)
const activeBeforeOverlay = await page.evaluate(
Expand All @@ -103,7 +105,9 @@ test.describe("OAuth Overlay", () => {

// Activate overlay
await setIsSyncing(page, true);
await page.waitForTimeout(100); // Give time for blur effect
await page.waitForFunction(
() => document.activeElement?.tagName === "BODY",
);

// Active element should be blurred (now body)
const activeAfterOverlay = await page.evaluate(
Expand Down
3 changes: 2 additions & 1 deletion e2e/tasks/delete-restore-task.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
expectTaskSavedToIndexedDB,
expectTaskVisible,
prepareTaskPage,
reloadTaskPage,
restoreDeletedTaskFromUndoToast,
} from "../utils/task-test-utils";

Expand Down Expand Up @@ -33,7 +34,7 @@ test.describe("Task Delete + Restore", () => {
await expectTaskVisible(page, taskTitle);
await expectTaskSavedToIndexedDB(page, taskTitle);

await page.reload();
await reloadTaskPage(page);
await expectTaskVisible(page, taskTitle, 10000);
await expectTaskSavedToIndexedDB(page, taskTitle);
});
Expand Down
3 changes: 2 additions & 1 deletion e2e/tasks/task-persistence.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
expectTaskSavedToIndexedDB,
expectTaskVisible,
prepareTaskPage,
reloadTaskPage,
} from "../utils/task-test-utils";

test.describe("Task Persistence", () => {
Expand All @@ -23,7 +24,7 @@ test.describe("Task Persistence", () => {
await expectTaskSavedToIndexedDB(page, taskTitle);

// Reload and verify persistence.
await page.reload();
await reloadTaskPage(page);
await expectTaskSavedToIndexedDB(page, taskTitle);
await expectTaskVisible(page, taskTitle, 10000);
});
Expand Down
4 changes: 2 additions & 2 deletions e2e/utils/event-test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ const retryUntil = async (
if (attempt === maxAttempts - 1) {
throw new Error(
`Action failed after ${maxAttempts} attempts. ` +
`Expected element to be visible: ${waitFor}`,
"Expected target element to become visible.",
);
}
await page.waitForTimeout(200);
Expand Down Expand Up @@ -182,7 +182,7 @@ export const resetLocalEventDb = async (page: Page) => {

deleteRequest.onsuccess = () => resolve();
deleteRequest.onerror = () => resolve();
deleteRequest.onblocked = async () => {
deleteRequest.onblocked = () => {
// If the app still has an open connection, fall back to clearing stores
// so tests still start from a clean state.
const openRequest = indexedDB.open(dbName);
Expand Down
148 changes: 45 additions & 103 deletions e2e/utils/task-test-utils.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,25 @@
import { type Page, expect } from "@playwright/test";
import { resetLocalEventDb } from "./event-test-utils";

const getTaskInput = (page: Page, title: string) =>
page.locator(`input[value="${title.replace(/"/g, '\\"')}"]`);

Check failure

Code scanning / CodeQL

Incomplete string escaping or encoding High

This does not escape backslash characters in the input.
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed

export const prepareTaskPage = async (page: Page) => {
await page.goto("/day", { waitUntil: "domcontentloaded" });
await page.evaluate(() => {
localStorage.removeItem("compass.auth");
});
await page.waitForURL(/\/day\/\d{4}-\d{2}-\d{2}$/);
await page.waitForFunction(
() => {
const root = document.querySelector("#root");
return root && root.children.length > 0;
},
{ timeout: 10000 },
);

await resetLocalEventDb(page);
await page.reload({ waitUntil: "domcontentloaded" });
await page.waitForURL(/\/day\/\d{4}-\d{2}-\d{2}$/);
await page.waitForFunction(
() => {
const root = document.querySelector("#root");
return root && root.children.length > 0;
},
{ timeout: 10000 },
);
await reloadTaskPage(page);
};

// Wait for task list to be visible
await expect(page.locator('[aria-label="daily-tasks"]')).toBeVisible();
export const reloadTaskPage = async (page: Page) => {
await page.goto("/day", { waitUntil: "domcontentloaded" });
await page.waitForURL(/\/day\/\d{4}-\d{2}-\d{2}$/);
await expect(page.locator('[aria-label="daily-tasks"]')).toBeVisible({
timeout: 15000,
});
};

export const createTask = async (page: Page, title: string) => {
Expand All @@ -54,21 +46,18 @@
title: string,
timeout = 10000,
) => {
await expect(
page.getByRole("textbox", { name: `Edit ${title}` }),
).toBeVisible({
await expect(page.locator('[aria-label="daily-tasks"]')).toBeVisible({
timeout,
});
await expect(getTaskInput(page, title)).toBeVisible({ timeout });
};

export const expectTaskMissing = async (
page: Page,
title: string,
timeout = 10000,
) => {
await expect(
page.getByRole("textbox", { name: `Edit ${title}` }),
).toHaveCount(0, {
await expect(getTaskInput(page, title)).toHaveCount(0, {
timeout,
});
};
Expand All @@ -84,106 +73,59 @@
};

export const restoreDeletedTaskFromUndoToast = async (page: Page) => {
const undoDeleteText = page.getByText("Deleted").first();
const undoDeleteToast = page
.getByRole("button", { name: /deleted/i })
.first();

await expect(undoDeleteText).toBeVisible();
await undoDeleteText.click();
await page.keyboard.press("Meta+z");
await page.keyboard.press("Control+z");
await expect(undoDeleteToast).toBeVisible();
await undoDeleteToast.click();
};

export const expectTaskSavedToIndexedDB = async (page: Page, title: string) => {
let lastSnapshot: unknown = null;

for (let attempt = 0; attempt < 20; attempt++) {
lastSnapshot = await page.evaluate(async () => {
const currentDateKey = window.location.pathname.split("/").pop() ?? "";

return await new Promise<{
openError?: boolean;
storeError?: string;
currentDateKey?: string;
tasks?: Array<{ title: string; dateKey: string }>;
}>((resolve) => {
const currentDateKey = page.url().split("/").pop() ?? "";

await page.waitForFunction(
async ([taskTitle, dateKey]) =>
new Promise<boolean>((resolve) => {
const openRequest = indexedDB.open("compass-local");
openRequest.onerror = () => resolve({ openError: true });
openRequest.onerror = () => resolve(false);
openRequest.onsuccess = () => {
const db = openRequest.result;

let store;
let transaction: IDBTransaction;
try {
const transaction = db.transaction("tasks", "readonly");
store = transaction.objectStore("tasks");
} catch (error) {
resolve({ storeError: String(error) });
transaction = db.transaction("tasks", "readonly");
} catch {
db.close();
resolve(false);
return;
}

const getAllRequest = store.getAll();
getAllRequest.onerror = () => resolve({ currentDateKey, tasks: [] });
const getAllRequest = transaction.objectStore("tasks").getAll();
getAllRequest.onerror = () => {
db.close();
resolve(false);
};
getAllRequest.onsuccess = () => {
const tasks = getAllRequest.result as Array<{
title?: string;
dateKey?: string;
}>;
resolve({
currentDateKey,
tasks: tasks
.filter(
(task): task is { title: string; dateKey: string } =>
Boolean(task?.title) && Boolean(task?.dateKey),
)
.map((task) => ({
title: task.title,
dateKey: task.dateKey,
})),
});
db.close();
resolve(
tasks.some(
(task) => task.title === taskTitle && task.dateKey === dateKey,
),
);
};
};
});
});

if (
typeof lastSnapshot === "object" &&
lastSnapshot !== null &&
"tasks" in lastSnapshot
) {
const tasks =
(
lastSnapshot as {
tasks?: Array<{ title: string; dateKey: string }>;
}
).tasks ?? [];
const currentDateKey =
(lastSnapshot as { currentDateKey?: string }).currentDateKey ?? "";

const isTaskSavedForCurrentDate = tasks.some(
(task) => task.title === title && task.dateKey === currentDateKey,
);

if (isTaskSavedForCurrentDate) {
return;
}
}

await page.waitForTimeout(200);
}

throw new Error(
`Task was not found in IndexedDB after polling: ${JSON.stringify(lastSnapshot)}`,
}),
[title, currentDateKey] as [string, string],
{ timeout: 5000 },
);
};

export const clearAllLocalData = async (page: Page) => {
await resetLocalEventDb(page);
await page.reload();
await expect(page.locator('[aria-label="daily-tasks"]')).toBeVisible();
};

/**
* @deprecated Use clearAllLocalData. This clears the shared local IndexedDB
* database (both events and tasks).
*/
export const clearTasks = async (page: Page) => {
await clearAllLocalData(page);
await reloadTaskPage(page);
};
4 changes: 0 additions & 4 deletions playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,6 @@ export default defineConfig({
name: "chromium-desktop",
use: { ...devices["Desktop Chrome"] },
},
{
name: "chromium-mobile",
use: { ...devices["Pixel 5"] },
},
],
webServer: {
command: "cd packages/web && bun run dev.ts",
Expand Down
Loading