import { Page, expect } from '@playwright/test'; /** * Helper functions for interacting with the Storyteller demo UI * These functions encapsulate common operations that are used across multiple tests */ /** * Sets the API key and environment via the Settings UI * @param page - The Playwright page object * @param apiKey - The API key to set * @param environment - The environment to set (default: 'production') */ export async function setApiKeyViaUI(page: Page, apiKey: string, environment: string = 'production') { console.log(`Setting API key via UI: ${apiKey}`); console.log(`Setting environment: ${environment}`); // Click the settings cog button const settingsCog = page.locator('button[aria-label="Settings"], button:has-text("⚙"), .settings-icon, [class*="settings"]').first(); await expect(settingsCog).toBeVisible({ timeout: 10000 }); await settingsCog.click(); // Wait for settings modal/panel to open await page.waitForTimeout(500); // Find and clear the API key input field const apiKeyInput = page.locator('input[placeholder*="API"], input[name*="apiKey"], input[id*="apiKey"], label:has-text("API Key") + input, label:has-text("API key") + input').first(); await expect(apiKeyInput).toBeVisible({ timeout: 5000 }); await apiKeyInput.clear(); await apiKeyInput.fill(apiKey); // Set the environment dropdown - look for the exact Environment label (not "Web SDK environment" or "AMP environment") const environmentLabel = page.locator('label').filter({ hasText: /^Environment$/ }); if (await environmentLabel.count() > 0) { // Find the select element in the same form group const environmentSelect = environmentLabel.locator('~ div select, ~ select').first(); if (await environmentSelect.isVisible()) { await environmentSelect.selectOption(environment); console.log(`Environment set to: ${environment}`); } } // Find and click the Save button const saveButton = page.locator('button:has-text("Save"), button[type="submit"]:has-text("Save")').first(); await expect(saveButton).toBeVisible(); await saveButton.click(); // Wait for settings to close and page to update await page.waitForTimeout(2000); console.log('API key and environment set successfully'); } /** * Sets the category for a specific story row via the Edit UI * @param page - The Playwright page object * @param rowIndex - The index of the row to edit (0-based) * @param category - The category to set */ export async function setCategoryViaUI(page: Page, rowIndex: number, category: string) { console.log(`Setting category for row ${rowIndex}: ${category}`); // Find all Edit buttons and click the one for the specified row const editButtons = page.locator('button:has-text("Edit")'); const editButtonCount = await editButtons.count(); if (rowIndex >= editButtonCount) { throw new Error(`Row index ${rowIndex} is out of bounds. Only ${editButtonCount} rows found.`); } await editButtons.nth(rowIndex).click(); // Wait for the edit panel to open await page.waitForSelector('.offcanvas.show, [class*="drawer"], [class*="panel"]', { timeout: 5000 }); await page.waitForTimeout(300); // Make sure we're on the General tab (not Theme tab) const generalTab = page.locator('button:has-text("General")'); if (await generalTab.isVisible()) { const isActive = await generalTab.evaluate(el => el.classList.contains('btn-primary') || el.classList.contains('active')); if (!isActive) { await generalTab.click(); await page.waitForTimeout(300); } } // Find the category input field // The field is typically the last text input when editing a story row const categoryLabel = page.locator('label:has-text("Category")'); let categoryInput; if (await categoryLabel.isVisible()) { // If we can find the label, get the associated input categoryInput = categoryLabel.locator('~ input[type="text"]').first(); } else { // Otherwise, use the last text input (common pattern in the demo) categoryInput = page.locator('.offcanvas input[type="text"], [class*="drawer"] input[type="text"], [class*="panel"] input[type="text"]').last(); } await expect(categoryInput).toBeVisible({ timeout: 5000 }); await categoryInput.clear(); await categoryInput.fill(category); // Find and click the submit button // The button text might be "Update Category" or just a generic submit const submitButton = page.locator('button:has-text("Update Category"), button[type="submit"]:not(:has-text("Update theme"))').first(); await expect(submitButton).toBeVisible(); await submitButton.click(); // Wait for the panel to close and SDK to reload stories await page.waitForTimeout(3000); console.log(`Category set successfully for row ${rowIndex}`); } /** * Creates a new view by clicking the "New view" button * @param page - The Playwright page object * @returns The index of the newly created view */ export async function createNewView(page: Page): Promise { console.log('Creating new view'); // Count existing views before adding const editButtonsBefore = page.locator('button:has-text("Edit")'); const countBefore = await editButtonsBefore.count(); // Click the New view button const newViewButton = page.locator('button:has-text("+ New view"), button:has-text("New view")'); await expect(newViewButton).toBeVisible({ timeout: 5000 }); await newViewButton.click(); // Wait for the new view to be created await page.waitForTimeout(500); // Verify a new view was added const editButtonsAfter = page.locator('button:has-text("Edit")'); const countAfter = await editButtonsAfter.count(); if (countAfter <= countBefore) { throw new Error('Failed to create new view'); } console.log(`New view created at index ${countAfter - 1}`); return countAfter - 1; // Return the index of the new view } /** * Configures a view's basic settings (media type, view type, tile type) and optionally category * @param page - The Playwright page object * @param rowIndex - The index of the row to configure * @param settings - The settings to apply */ export async function configureViewSettings( page: Page, rowIndex: number, settings: { mediaType?: 'stories' | 'clips'; viewType?: 'row' | 'grid'; tileType?: 'round' | 'square'; category?: string; } ) { console.log(`Configuring view settings for row ${rowIndex}:`, settings); // Open the edit panel const editButtons = page.locator('button:has-text("Edit")'); await editButtons.nth(rowIndex).click(); // Wait for panel to open await page.waitForSelector('.offcanvas.show', { timeout: 5000 }); await page.waitForTimeout(300); // Make sure we're on General tab const generalTab = page.locator('button:has-text("General")'); if (await generalTab.isVisible()) { const isActive = await generalTab.evaluate(el => el.classList.contains('btn-primary') || el.classList.contains('active')); if (!isActive) { await generalTab.click(); await page.waitForTimeout(300); } } // Set media type if provided if (settings.mediaType) { const mediaTypeSelect = page.locator('select[aria-label="Media type"]'); await mediaTypeSelect.selectOption(settings.mediaType); await page.waitForTimeout(200); } // Set view type if provided if (settings.viewType) { const viewTypeSelect = page.locator('select[aria-label="View type"]'); await viewTypeSelect.selectOption(settings.viewType); await page.waitForTimeout(200); } // Set tile type if provided (only for stories row view) if (settings.tileType && settings.mediaType === 'stories' && settings.viewType === 'row') { const tileTypeSelect = page.locator('select[aria-label="Tile type"]'); await expect(tileTypeSelect).toBeVisible({ timeout: 5000 }); await tileTypeSelect.selectOption(settings.tileType); } // Set category if provided if (settings.category) { const categoryInput = page.locator('.offcanvas input[type="text"]').last(); await categoryInput.clear(); await categoryInput.fill(settings.category); console.log(`Setting category: ${settings.category}`); } // Submit the form to save settings and close the panel const submitButton = page.locator('button[type="submit"]:not(:has-text("Update theme"))').first(); await submitButton.click(); await page.waitForTimeout(2000); console.log('View settings configured'); } /** * Applies a theme to a view via the Theme tab * @param page - The Playwright page object * @param rowIndex - The index of the row to theme * @param themeJson - The theme JSON to apply */ export async function applyThemeToView(page: Page, rowIndex: number, themeJson: object) { console.log(`Applying theme to row ${rowIndex}`); // Open edit panel if not already open const offcanvas = page.locator('.offcanvas.show'); if (!(await offcanvas.isVisible())) { const editButtons = page.locator('button:has-text("Edit")'); await editButtons.nth(rowIndex).click(); await page.waitForSelector('.offcanvas.show', { timeout: 5000 }); } // Switch to Theme tab const themeTab = page.locator('button:has-text("Theme")'); await expect(themeTab).toBeVisible(); await themeTab.click(); await page.waitForTimeout(300); // Set the theme JSON const themeTextarea = page.locator('textarea').first(); await themeTextarea.clear(); await themeTextarea.fill(JSON.stringify(themeJson, null, 2)); // Apply the theme const updateThemeButton = page.locator('button:has-text("Update theme")'); await updateThemeButton.click(); await page.waitForTimeout(1000); console.log('Theme applied successfully'); } /** * Waits for story cells to load with actual content * @param page - The Playwright page object * @param timeout - Maximum time to wait in milliseconds * @returns The number of loaded cells */ export async function waitForStoryCells(page: Page, timeout: number = 10000): Promise { console.log('Waiting for story cells to load...'); try { // Wait for cells to appear - be very lenient await page.waitForFunction( () => { // Look for any story-related elements const cells = document.querySelectorAll('.roundCellContainer, [class*="roundCell"], [class*="storyCell"], .story-tile, [id*="storyteller"]'); return cells.length > 0; }, { timeout } ); } catch (e) { console.log('No story cells found within timeout'); return 0; } // Get the final count const cellCount = await page.evaluate(() => { const cells = document.querySelectorAll('.roundCellContainer, [class*="roundCell"], [class*="storyCell"], .story-tile'); return cells.length; }); console.log(`${cellCount} story cells loaded`); return cellCount; } /** * Measures the size of story cells * @param page - The Playwright page object * @returns Object with cell measurements */ export async function measureStoryCells(page: Page) { return await page.evaluate(() => { const cells = document.querySelectorAll('.roundCellContainer, [class*="roundCell"], [class*="storyCell"]'); if (cells.length === 0) { return null; } const firstCell = cells[0] as HTMLElement; const rect = firstCell.getBoundingClientRect(); return { width: rect.width, height: rect.height, count: cells.length }; }); }