mirror of
https://github.com/Dicklesworthstone/coding_agent_session_search.git
synced 2026-09-22 11:59:25 +08:00
f85dcbe20b
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>
335 lines
11 KiB
TypeScript
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);
|
|
}
|
|
});
|
|
});
|