Files
Cass Agent f85dcbe20b style(js): prettier wrap pages assets, e2e tests, and CSS
Application pages JS, service worker, CSS, and the e2e/unit test tree
were rewritten by the same formatter pass as the vendor files: quotes,
wrapping, trailing commas, and import sort. Viewer behavior is
unchanged.

- src/pages_assets/*.js, styles.css, sw.js, virtual-list.test.html
- tests/e2e, tests/html_export, tests/pages-*.mjs, tests/performance
- tests/accessibility, tests/playwright.config.ts

Co-authored-by: Grok <noreply@x.ai>
2026-09-21 10:07:26 -04:00

335 lines
11 KiB
TypeScript

import { expect, gotoFile, test, waitForPageReady } from "../setup/test-utils";
/**
* Mobile device E2E tests - Virtual keyboard interactions
*
* Tests that form inputs work correctly with virtual keyboard
* behavior typical of mobile devices.
*/
test.describe("Virtual Keyboard Behavior", () => {
test.beforeEach(async ({ page }) => {
const viewport = page.viewportSize();
console.log(
`[device-context] Testing keyboard behavior at ${viewport?.width}x${viewport?.height}`,
);
});
test("input fields respond to focus tap", async ({ page, exportPath }) => {
test.skip(!exportPath, "Export path not available");
await gotoFile(page, exportPath);
await waitForPageReady(page);
const inputs = page.locator(
'input:not([type="hidden"]):not([type="checkbox"]):not([type="radio"])',
);
const inputCount = await inputs.count();
if (inputCount > 0) {
const firstInput = inputs.first();
// Tap to focus
await firstInput.tap();
await page.waitForTimeout(200);
// Check if focused
const isFocused = await firstInput.evaluate((el) => el === document.activeElement);
expect(isFocused).toBe(true);
}
});
test("viewport adjusts for keyboard", async ({ page, exportPath }) => {
test.skip(!exportPath, "Export path not available");
await gotoFile(page, exportPath);
await waitForPageReady(page);
const searchInput = page.locator('#search-input, input[type="search"]');
if ((await searchInput.count()) === 0) {
test.skip(true, "Search input not found");
return;
}
// Get initial visual viewport height
const initialViewport = await page.evaluate(() => ({
visual: window.visualViewport?.height || window.innerHeight,
layout: window.innerHeight,
}));
// Focus input (simulates keyboard opening)
await searchInput.first().tap();
await page.waitForTimeout(300);
// Simulate keyboard opening by reducing viewport
// (Playwright doesn't actually open a virtual keyboard, so we simulate)
const keyboardHeight = 300;
const currentViewport = page.viewportSize();
if (currentViewport) {
await page.setViewportSize({
width: currentViewport.width,
height: currentViewport.height - keyboardHeight,
});
}
await page.waitForTimeout(200);
// The focused input should still be visible
const inputIsVisible = await searchInput.first().isVisible();
expect(inputIsVisible).toBe(true);
// The input should be within the visible area
const inputBox = await searchInput.first().boundingBox();
const newViewport = page.viewportSize();
if (inputBox && newViewport) {
// Input should be within viewport bounds
expect(inputBox.y + inputBox.height).toBeLessThanOrEqual(newViewport.height);
}
// Restore viewport
if (currentViewport) {
await page.setViewportSize(currentViewport);
}
});
test("form submission works with enter key", async ({ page, exportPath }) => {
test.skip(!exportPath, "Export path not available");
await gotoFile(page, exportPath);
await waitForPageReady(page);
const searchInput = page.locator('#search-input, input[type="search"]');
if ((await searchInput.count()) === 0) {
test.skip(true, "Search input not found");
return;
}
await searchInput.first().tap();
await page.keyboard.type("token", { delay: 50 });
// Press Enter (simulates virtual keyboard "Go"/"Search" button)
await page.keyboard.press("Enter");
await page.waitForTimeout(500);
// Verify search was triggered (look for results or state change)
const searchTriggered = await page.evaluate(() => {
// Check URL for search param
const url = new URL(window.location.href);
if (url.searchParams.has("q") || url.searchParams.has("search")) {
return true;
}
// Check for highlights
const highlights = document.querySelectorAll(
"mark, .highlight, .search-highlight, .search-match, .message.search-hit",
);
return highlights.length > 0;
});
expect(searchTriggered).toBe(true);
});
test("password input masks characters", async ({ page, encryptedExportPath }) => {
test.skip(!encryptedExportPath, "Encrypted export not available");
await gotoFile(page, encryptedExportPath);
await waitForPageReady(page);
const passwordInput = page.locator('input[type="password"]');
if ((await passwordInput.count()) === 0) {
test.skip(true, "Password input not found");
return;
}
// Type password
await passwordInput.first().tap();
await page.keyboard.type("secretpass123", { delay: 30 });
// Check that value is masked (can't actually verify masking visually,
// but we can verify the input type is still password)
const inputType = await passwordInput.first().getAttribute("type");
expect(inputType).toBe("password");
// Value should be stored
const value = await passwordInput.first().inputValue();
expect(value).toBe("secretpass123");
});
test("input clear button works", async ({ page, exportPath }) => {
test.skip(!exportPath, "Export path not available");
await gotoFile(page, exportPath);
await waitForPageReady(page);
const searchInput = page.locator('#search-input, input[type="search"]');
if ((await searchInput.count()) === 0) {
test.skip(true, "Search input not found");
return;
}
// Type some text
await searchInput.first().tap();
await page.keyboard.type("test");
await page.waitForTimeout(100);
// Look for clear button
const clearButton = page.locator(
'[data-action="clear"], .clear-btn, .search-clear, input[type="search"]::-webkit-search-cancel-button',
);
// Try to find a clickable clear button
const clearBtnVisible = page.locator('button:near(#search-input), [aria-label*="clear" i]');
if ((await clearBtnVisible.count()) > 0) {
await clearBtnVisible.first().tap();
await page.waitForTimeout(100);
const value = await searchInput.first().inputValue();
const searchStateReset = await page.evaluate(() => {
const url = new URL(window.location.href);
const hasQueryParam = url.searchParams.has("q") || url.searchParams.has("search");
const highlights = document.querySelectorAll("mark, .highlight, .search-match");
return !hasQueryParam && highlights.length === 0;
});
expect(value === "" || searchStateReset).toBe(true);
}
});
test("autocomplete suggestions are tappable", async ({ page, exportPath }) => {
test.skip(!exportPath, "Export path not available");
await gotoFile(page, exportPath);
await waitForPageReady(page);
const searchInput = page.locator('#search-input, input[type="search"]');
if ((await searchInput.count()) === 0) {
test.skip(true, "Search input not found");
return;
}
await searchInput.first().tap();
await page.keyboard.type("fu", { delay: 100 });
await page.waitForTimeout(300);
// Look for autocomplete suggestions
const suggestions = page.locator(
'[role="listbox"] [role="option"], .autocomplete-item, .suggestion, .search-suggestion',
);
if ((await suggestions.count()) > 0) {
const firstSuggestion = suggestions.first();
// Should be visible and tappable
await expect(firstSuggestion).toBeVisible();
const box = await firstSuggestion.boundingBox();
if (box) {
// Tap target should be adequate size
expect(Math.min(box.width, box.height)).toBeGreaterThanOrEqual(32);
// Tap the suggestion
await firstSuggestion.tap();
await page.waitForTimeout(200);
// Input should have been populated
const value = await searchInput.first().inputValue();
expect(value.length).toBeGreaterThan(0);
}
}
});
});
test.describe("Form Field Navigation", () => {
test("tab navigation works between fields", async ({ page, exportPath }) => {
test.skip(!exportPath, "Export path not available");
await gotoFile(page, exportPath);
await waitForPageReady(page);
// Find all focusable elements
const focusable = page.locator(
'input:not([type="hidden"]), button, a[href], textarea, select, [tabindex]:not([tabindex="-1"])',
);
const count = await focusable.count();
if (count > 1) {
// Focus first element
await focusable.first().tap();
await page.waitForTimeout(100);
// Tab to next
await page.keyboard.press("Tab");
await page.waitForTimeout(100);
// Something should be focused
const hasFocus = await page.evaluate(() => {
return document.activeElement !== document.body;
});
expect(hasFocus).toBe(true);
}
});
test("shift+tab navigates backwards", async ({ page, exportPath }) => {
test.skip(!exportPath, "Export path not available");
await gotoFile(page, exportPath);
await waitForPageReady(page);
const focusable = page.locator('input:not([type="hidden"]), button, a[href], textarea');
const count = await focusable.count();
if (count > 2) {
// Focus second element
await focusable.nth(1).focus();
const beforeIndex = await focusable.evaluateAll((els) =>
els.findIndex((el) => el === document.activeElement),
);
// Shift+Tab to go back
await page.keyboard.press("Shift+Tab");
await page.waitForTimeout(100);
const afterIndex = await focusable.evaluateAll((els) =>
els.findIndex((el) => el === document.activeElement),
);
expect(afterIndex).toBeGreaterThanOrEqual(0);
expect(afterIndex).not.toBe(beforeIndex);
}
});
test("escape closes dropdown/autocomplete", async ({ page, exportPath }) => {
test.skip(!exportPath, "Export path not available");
await gotoFile(page, exportPath);
await waitForPageReady(page);
const searchInput = page.locator('#search-input, input[type="search"]');
if ((await searchInput.count()) === 0) {
test.skip(true, "Search input not found");
return;
}
await searchInput.first().tap();
await page.keyboard.type("test");
await page.waitForTimeout(200);
// Look for any open dropdown/autocomplete
const dropdown = page.locator('[role="listbox"], .dropdown-menu, .autocomplete-list');
if ((await dropdown.count()) > 0) {
// Press Escape
await page.keyboard.press("Escape");
await page.waitForTimeout(200);
// Dropdown should be closed or hidden
const isHidden = await dropdown.first().evaluate((el) => {
const style = window.getComputedStyle(el);
return style.display === "none" || style.visibility === "hidden";
});
expect(isHidden).toBe(true);
}
});
});