Files
noteflow/client/e2e-native-mac/fixtures.ts
Travis Vasceannie 100ca5596b
Some checks failed
CI / test-python (push) Failing after 16m26s
CI / test-rust (push) Has been cancelled
CI / test-typescript (push) Has been cancelled
mac
2026-01-24 12:47:35 -05:00

331 lines
9.7 KiB
TypeScript

/**
* Mac Native E2E Test Fixtures (Appium mac2 driver).
*
* These helpers interact with the macOS accessibility tree exposed by the WebView.
*/
/** Timeout constants for E2E test operations */
const Timeouts = {
/** Default timeout for element searches */
DEFAULT_ELEMENT_WAIT_MS: 10000,
/** Extended timeout for app startup */
APP_READY_WAIT_MS: 30000,
/** Delay after navigation for animation completion */
NAVIGATION_ANIMATION_MS: 300,
/** Delay after tab switch for animation completion */
TAB_SWITCH_ANIMATION_MS: 200,
} as const;
/** Generate predicate selectors for finding elements by label/title/identifier/value */
const labelSelectors = (label: string): string[] => [
// mac2 driver uses 'label' and 'identifier' attributes, not 'type' or 'name'
`-ios predicate string:label == "${label}"`,
`-ios predicate string:title == "${label}"`,
`-ios predicate string:name == "${label}"`,
`-ios predicate string:identifier == "${label}"`,
`-ios predicate string:value == "${label}"`,
`~${label}`,
];
/** Generate predicate selectors for partial text matching */
const containsSelectors = (text: string): string[] => [
`-ios predicate string:label CONTAINS "${text}"`,
`-ios predicate string:title CONTAINS "${text}"`,
`-ios predicate string:name CONTAINS "${text}"`,
`-ios predicate string:value CONTAINS "${text}"`,
];
/** Generate predicate selectors for placeholder text */
const placeholderSelectors = (placeholder: string): string[] => [
`-ios predicate string:placeholderValue == "${placeholder}"`,
`-ios predicate string:value == "${placeholder}"`,
];
/** Find first displayed element from a list of selectors */
async function findDisplayedElement(selectors: string[]): Promise<WebdriverIO.Element | null> {
for (const selector of selectors) {
const elements = await $$(selector);
for (const element of elements) {
if (await element.isDisplayed()) {
return element;
}
}
}
return null;
}
/** Find all displayed elements matching any of the selectors */
async function findAllDisplayedElements(selectors: string[]): Promise<WebdriverIO.Element[]> {
const results: WebdriverIO.Element[] = [];
for (const selector of selectors) {
const elements = await $$(selector);
for (const element of elements) {
if (await element.isDisplayed()) {
results.push(element);
}
}
}
return results;
}
/**
* Wait for an element with the given label to be displayed.
* Tries multiple selector strategies (label, title, identifier, value, accessibility id).
*/
export async function waitForLabel(
label: string,
timeout = Timeouts.DEFAULT_ELEMENT_WAIT_MS
): Promise<WebdriverIO.Element> {
let found: WebdriverIO.Element | null = null;
await browser.waitUntil(
async () => {
found = await findDisplayedElement(labelSelectors(label));
return Boolean(found);
},
{
timeout,
timeoutMsg: `Element with label "${label}" not found within ${timeout}ms`,
}
);
// Element is guaranteed non-null after waitUntil succeeds
return found as WebdriverIO.Element;
}
/**
* Wait for the first available element from a list of labels.
*/
export async function waitForAnyLabel(
labels: string[],
timeout = Timeouts.DEFAULT_ELEMENT_WAIT_MS
): Promise<WebdriverIO.Element> {
const selectors = labels.flatMap((label) => labelSelectors(label));
let found: WebdriverIO.Element | null = null;
await browser.waitUntil(
async () => {
found = await findDisplayedElement(selectors);
return Boolean(found);
},
{
timeout,
timeoutMsg: `None of the labels found within ${timeout}ms: ${labels.join(', ')}`,
}
);
return found as WebdriverIO.Element;
}
/**
* Wait for an element containing the given text to be displayed.
*/
export async function waitForTextContaining(
text: string,
timeout = Timeouts.DEFAULT_ELEMENT_WAIT_MS
): Promise<WebdriverIO.Element> {
let found: WebdriverIO.Element | null = null;
await browser.waitUntil(
async () => {
found = await findDisplayedElement(containsSelectors(text));
return Boolean(found);
},
{
timeout,
timeoutMsg: `Element containing text "${text}" not found within ${timeout}ms`,
}
);
// Element is guaranteed non-null after waitUntil succeeds
return found as WebdriverIO.Element;
}
/**
* Check if an element with the given label exists and is displayed.
*/
export async function isLabelDisplayed(label: string): Promise<boolean> {
const element = await findDisplayedElement(labelSelectors(label));
return element !== null;
}
/**
* Check if an element containing the given text exists and is displayed.
*/
export async function isTextDisplayed(text: string): Promise<boolean> {
const element = await findDisplayedElement(containsSelectors(text));
return element !== null;
}
/**
* Click an element with the given label.
*/
export async function clickByLabel(
label: string,
timeout = Timeouts.DEFAULT_ELEMENT_WAIT_MS
): Promise<void> {
const element = await waitForLabel(label, timeout);
await element.click();
}
/**
* Click the first matching label from a list of options.
*/
export async function clickByAnyLabel(
labels: string[],
timeout = Timeouts.DEFAULT_ELEMENT_WAIT_MS
): Promise<void> {
const element = await waitForAnyLabel(labels, timeout);
await element.click();
}
/**
* Click an element containing the given text.
*/
export async function clickByText(
text: string,
timeout = Timeouts.DEFAULT_ELEMENT_WAIT_MS
): Promise<void> {
const element = await waitForTextContaining(text, timeout);
await element.click();
}
/**
* Wait for the app to be ready (main shell visible).
*/
export async function waitForAppReady(): Promise<void> {
await waitForLabel('NoteFlow', Timeouts.APP_READY_WAIT_MS);
}
/**
* Navigate to a page via sidebar link.
* @param pageName The visible label of the navigation item (e.g., 'Home', 'Settings', 'Projects')
*/
export async function navigateToPage(pageName: string): Promise<void> {
const navId = `nav-${pageName.toLowerCase().replace(/\s+/g, '-')}`;
await clickByAnyLabel([navId, pageName]);
// Small delay for navigation animation
await browser.pause(Timeouts.NAVIGATION_ANIMATION_MS);
}
/**
* Click a tab in a tab list.
* @param tabName The visible label of the tab (e.g., 'Status', 'Audio', 'AI')
*/
export async function clickTab(tabName: string): Promise<void> {
await clickByLabel(tabName);
// Small delay for tab switch animation
await browser.pause(Timeouts.TAB_SWITCH_ANIMATION_MS);
}
/**
* Find an input field by placeholder and type text into it.
* @param placeholder The placeholder text of the input
* @param text The text to type
*/
export async function typeIntoInput(placeholder: string, text: string): Promise<void> {
const selectors = placeholderSelectors(placeholder);
let input: WebdriverIO.Element | null = null;
await browser.waitUntil(
async () => {
input = await findDisplayedElement(selectors);
return Boolean(input);
},
{
timeout: Timeouts.DEFAULT_ELEMENT_WAIT_MS,
timeoutMsg: `Input with placeholder "${placeholder}" not found`,
}
);
// Input is guaranteed non-null after waitUntil succeeds
const inputElement = input as WebdriverIO.Element;
await inputElement.click();
await inputElement.setValue(text);
}
/**
* Clear an input field by placeholder.
* @param placeholder The placeholder text of the input
*/
export async function clearInput(placeholder: string): Promise<void> {
const selectors = placeholderSelectors(placeholder);
const input = await findDisplayedElement(selectors);
if (input) {
await input.click();
await input.clearValue();
}
}
/**
* Find and click a button by its text content.
* @param buttonText The text on the button
*/
export async function clickButton(
buttonText: string,
timeout = Timeouts.DEFAULT_ELEMENT_WAIT_MS
): Promise<void> {
await clickByLabel(buttonText, timeout);
}
/**
* Wait for a label to disappear from the screen.
* @param label The label text to wait for disappearance
*/
export async function waitForLabelToDisappear(
label: string,
timeout = Timeouts.DEFAULT_ELEMENT_WAIT_MS
): Promise<void> {
await browser.waitUntil(
async () => {
const displayed = await isLabelDisplayed(label);
return !displayed;
},
{
timeout,
timeoutMsg: `Element with label "${label}" did not disappear within ${timeout}ms`,
}
);
}
/**
* Count the number of displayed elements matching a label.
* @param label The label to search for
*/
export async function countElementsByLabel(label: string): Promise<number> {
const elements = await findAllDisplayedElements(labelSelectors(label));
return elements.length;
}
/**
* Get all displayed text values matching a pattern.
* Useful for verifying lists of items.
*/
export async function getDisplayedTexts(pattern: string): Promise<string[]> {
const elements = await findAllDisplayedElements(containsSelectors(pattern));
const texts: string[] = [];
for (const element of elements) {
const text = await element.getText();
if (text) {
texts.push(text);
}
}
return texts;
}
/**
* Take a screenshot with a descriptive name.
* @param name Description of what the screenshot captures
*/
export async function takeScreenshot(name: string): Promise<void> {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const filename = `${name}-${timestamp}.png`;
await browser.saveScreenshot(`./e2e-native-mac/screenshots/${filename}`);
}
/**
* Verify an element is visible and get its text content.
* @param label The label of the element
*/
export async function getElementText(label: string): Promise<string | null> {
const element = await findDisplayedElement(labelSelectors(label));
if (!element) {
return null;
}
return element.getText();
}