331 lines
9.7 KiB
TypeScript
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();
|
|
}
|