File size: 17,257 Bytes
6efe519 44360b4 aa2c02d 44360b4 6efe519 38e3f0b 6efe519 cb40b21 44360b4 cb40b21 44360b4 19c6fcb 9945310 6efe519 cb40b21 44360b4 6efe519 cb40b21 44360b4 cb40b21 44360b4 cb40b21 8137781 cb40b21 19c6fcb 1b8f9df 19c6fcb 44360b4 19c6fcb 44360b4 19c6fcb 8d6e091 19c6fcb 734545f c25e1ae 6633e5d 8d6e091 6633e5d 44360b4 9945310 44360b4 9945310 44360b4 9945310 44360b4 c25e1ae 6633e5d c25e1ae 6633e5d c25e1ae 44360b4 fe46f97 c25e1ae 8d6e091 eba6457 8d6e091 44360b4 9945310 78fd0cb 9945310 78fd0cb 9945310 78fd0cb 9945310 aa2c02d 9945310 aa2c02d 9518e44 aa2c02d 9518e44 aa2c02d 9518e44 ef61044 9518e44 aa2c02d ef61044 74e4412 9518e44 aa2c02d 74e4412 aa2c02d 9518e44 74e4412 9518e44 78fd0cb 9518e44 c25e1ae |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 |
import fs from 'node:fs'
import * as crypto from "node:crypto";
import { Locator, Page, TestInfo, expect } from '@playwright/test';
interface CellObject {
table: number
row: number,
word: string
}
interface CurrentTableContent {
count: number,
word_prefix: string
}
export interface ArrayTables {
sort_order: string,
array: CurrentTableContent[]
}
export type role = "alert" | "alertdialog" | "application" | "article" | "banner" | "blockquote" | "button" | "caption" | "cell" | "checkbox" | "code" | "columnheader" | "combobox" | "complementary" | "contentinfo" | "definition" | "deletion" | "dialog" | "directory" | "document" | "emphasis" | "feed" | "figure" | "form" | "generic" | "grid" | "gridcell" | "group" | "heading" | "img" | "insertion" | "link" | "list" | "listbox" | "listitem" | "log" | "main" | "marquee" | "math" | "meter" | "menu" | "menubar" | "menuitem" | "menuitemcheckbox" | "menuitemradio" | "navigation" | "none" | "note" | "option" | "paragraph" | "presentation" | "progressbar" | "radio" | "radiogroup" | "region" | "row" | "rowgroup" | "rowheader" | "scrollbar" | "search" | "searchbox" | "separator" | "slider" | "spinbutton" | "status" | "strong" | "subscript" | "superscript" | "switch" | "tab" | "table" | "tablist" | "tabpanel" | "term" | "textbox" | "time" | "timer" | "toolbar" | "tooltip" | "tree" | "treegrid" | "treeitem"
interface CellArray {
table: number;
row: number;
word: string;
}
export type ScrollToPosition = "top" | "bottom";
export type ClickOrEnter = "click" | "Enter"
export const fileReader = async (filePath: string): Promise<string> => {
try {
const data = fs.readFileSync(filePath, { encoding: 'utf8' });
console.log(`fileReader::data length:", '${data.length}'`);
return data
} catch (err) {
console.error("fileReader::err:", err, "#");
throw err
}
}
export const fileWriter = async (filePath: string, data: string): Promise<void> => {
try {
fs.writeFileSync(filePath, data, "utf8");
console.log(`fileWriter::File written to ${filePath}...`);
} catch (err) {
console.error("fileWriter::err:", err, "#");
throw err;
}
}
export const loopOverTablesAndClickOnUrls = async (page: Page, cellObj: CellObject, timeout = 50, ariaSnapshotName: string) => {
let cellLabel = `id-table-${cellObj["table"]}-row-${cellObj["row"]}-nth`
try {
console.log(`current aria-label:${cellLabel}...`)
console.log(`current cell content: '${cellLabel}'...`)
let currentCellElement = page.getByLabel(cellLabel).locator('a')
console.log("currentCellElement:", currentCellElement, "#")
let currentInnerText = await currentCellElement.innerText()
console.log(`currentCellElement::innerText: '${currentInnerText}'`)
expect(currentInnerText).toBe(cellObj.word)
await currentCellElement.click({ timeout: 1000 });
await page.waitForTimeout(timeout)
await expect(page.getByLabel('editor')).toMatchAriaSnapshot({ name: ariaSnapshotName });
} catch (err) {
console.log("cellLabel:", cellLabel, "#")
console.log("err:", err, "#")
throw err
}
}
export const assertTableStap = async (page: Page, count: number, sortOrder: string, testIdx: number, subFolderName: string, action: string) => {
let containerTables = page.getByLabel('words-frequency', { exact: true })
let tablesArray = containerTables.getByRole("table")
let tablesArrayLen = await tablesArray.count()
console.log("tablesArrayLen:", tablesArrayLen, "#")
await expect(tablesArray).toHaveCount(count)
let containerTablesAriaSnap = await containerTables.ariaSnapshot()
if (action === "read") {
const ariaSnapshot = await fileReader(`${import.meta.dirname}/${subFolderName}/test-${testIdx}-${sortOrder}.txt`)
expect(containerTablesAriaSnap).toBe(ariaSnapshot)
} else if (action === "write") {
// the automatic aria snapshot test save system doesn't work, we save it manually test-words-frequency-2-filtering-sorting-snapshots
fileWriter(`${import.meta.dirname}/${subFolderName}/test-${testIdx}-${sortOrder}.txt`, containerTablesAriaSnap)
} else {
throw Error(`Wrong condition: '${action}'`)
}
}
export async function testWithLoop(page: Page, testLLMTextFilePath: string, cellArray2: CellArray[], assertTitleString: string) {
// await page.goto(process.env.DOMAIN_PORT ?? "/");
console.log(page.url())
console.log("Let's try with a much longer, multiline text while scrolling the conteditable div on click")
console.log("first upload a new, longer, multiline text then populate again the words frequency tables and re-try again the word links")
const fileChooserPromise = page.waitForEvent('filechooser');
await page.getByRole('button', { name: 'id-input-file-selector' }).click();
await page.waitForTimeout(200)
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(testLLMTextFilePath);
await page.waitForTimeout(200)
await page.getByRole('button', { name: 'btn4-get-words-frequency' }).click();
const wordsFreqTableTitle = page.getByLabel('id-words-frequency-table-title')
console.log("assertTitleString:", assertTitleString, "#")
await expect(wordsFreqTableTitle).toContainText(assertTitleString);
const editor = page.getByLabel("editor");
// avoid snapshot differences due to spellcheck
editor.evaluate((el: HTMLDivElement) => el.setAttribute("spellcheck", "false"))
await page.waitForTimeout(100)
console.log("try with a new array of tables/rows...")
for (let idx in cellArray2) {
await loopOverTablesAndClickOnUrls(page, cellArray2[idx], 100, `test-loop-${assertTitleString}-${idx}.txt`)
}
console.log("end!")
}
export async function assertCellAndLink(page: Page, gameEditor: Locator, idCell: string, expectedCellString: string, assertScreenshot: boolean = true) {
let tableOfWordsElNth0 = page.getByLabel(idCell).getByRole('cell');
await expect(tableOfWordsElNth0).toMatchAriaSnapshot(`- cell "${idCell}-link": "${expectedCellString}"`);
await page.getByLabel(`${idCell}-link`).click();
await page.waitForTimeout(100);
if (assertScreenshot) {
await expect(gameEditor).toHaveScreenshot();
}
}
export async function assertCellAndLinkAriaSnapshot(page: Page, idCell: string, expectedCellString: string, idElementSnapshot: string, expectedSnapshotString: string) {
// await assertCellAndLink(page, page.locator("not_used"), idCell, expectedCellString, false)
let tableOfWordsElNth0 = page.getByLabel(idCell).getByRole('cell');
await expect(tableOfWordsElNth0).toMatchAriaSnapshot(`- cell "${idCell}-link": "${expectedCellString}"`);
await page.getByLabel(`${idCell}-link`).click();
await page.waitForTimeout(100);
await expectOnlyVisibleTextInElement(page, idElementSnapshot, expectedSnapshotString)
}
export async function expectVisibleTextWithWalker(
page: Page,
idElement: string,
expectedString: string,
timeout = 10000
): Promise<void> {
// First, check that the text is present in the DOM
console.log(`expectVisibleTextWithWalker::start:${idElement} => ${expectedString} #`)
const loc = page.locator(`#${idElement}`)
try {
await expect(loc).toContainText(expectedString)
} catch {
console.error(`expectVisibleTextWithWalker, idElement ${idElement} allTextContents:`, await loc.allTextContents(), "#")
}
console.log(`expectVisibleTextWithWalker::found expectedString, go ahead!`)
await page.waitForFunction(
({ idElement, expected }: { idElement: string; expected: string }) => {
const container = document.getElementById(idElement);
if (!!container && !container.textContent?.includes(expected)) {
console.error("expectVisibleTextWithWalker::DEBUG:expected:", expected, "#")
}
return !!container && container.textContent?.includes(expected);
},
{ idElement, expected: expectedString },
{ timeout }
);
// Next, check that the element is scrolled so that the expected text is visible in the viewport
await page.waitForFunction(
({ idElement, expected }: { idElement: string; expected: string }) => {
const container = document.getElementById(idElement);
if (!container) return false;
const parentRect = container.getBoundingClientRect();
let visible = '';
function getVisibleText(node: Node): string {
if (node.nodeType === Node.TEXT_NODE) {
const range = document.createRange();
range.selectNode(node);
const rects = range.getClientRects();
for (const rect of rects) {
if (
rect.bottom > parentRect.top &&
rect.top < parentRect.bottom &&
rect.right > parentRect.left &&
rect.left < parentRect.right
) {
return node.textContent ?? '';
}
}
return '';
} else if (node.nodeType === Node.ELEMENT_NODE) {
let text = '';
for (const child of (node as Element).childNodes) {
text += getVisibleText(child);
}
return text;
}
return '';
}
visible = getVisibleText(container);
// Check that the visible portion contains the expected string
return visible.includes(expected);
},
{ idElement, expected: expectedString },
{ timeout }
);
// Optionally, assert the scroll position is not at the top or bottom (unless that's expected)
// For example, you can check that the scrollTop is not zero (not at top)
const scrolled = await page.evaluate((id) => {
const el = document.getElementById(id);
if (!el) return null;
return { scrollTop: el.scrollTop, scrollHeight: el.scrollHeight, clientHeight: el.clientHeight };
}, idElement);
if (scrolled) {
// You can add more specific assertions here if you know the expected scroll position
// For now, just log the scroll state
console.error(`Scroll state for #${idElement}:`, scrolled);
}
}
/**
* @param page Playwright Page object
* @param idElement The id of the element to check
* @param expectedVisible The exact string expected to be visible in the viewport
*/
export async function expectOnlyVisibleTextInElement(
page: Page,
idElement: string,
expectedVisible: string
) {
// Use bounding rects to get visible text for complex HTML
const visibleText = await page.evaluate((id) => {
const el = document.getElementById(id);
if (!el) {
throw Error(`HTML element with id '${id}' not found!`)
}
const parentRect = el.getBoundingClientRect();
let visible = '';
function getVisibleText(node: Node): string {
if (node.nodeType === Node.TEXT_NODE) {
const range = document.createRange();
range.selectNode(node);
const rects = range.getClientRects();
for (const rect of rects) {
if (
rect.bottom > parentRect.top &&
rect.top < parentRect.bottom &&
rect.right > parentRect.left &&
rect.left < parentRect.right
) {
return node.textContent || '';
}
}
return '';
} else if (node.nodeType === Node.ELEMENT_NODE) {
let text = '';
for (const child of (node as Element).childNodes) {
text += getVisibleText(child);
}
return text;
}
return '';
}
visible = getVisibleText(el);
return visible.trim();
}, idElement);
const rndString = crypto.randomBytes(20).toString('hex');
expect(visibleText).not.toBe(expectedVisible + ` - STRING TO NOT MATCH: ${rndString}!`);
// we'll check only if the expected string is within the Page element, just to try handling devices with different viewports
try {
expect(visibleText).toContain(expectedVisible);
} catch (err) {
console.log("expectedVisible:", typeof expectedVisible, expectedVisible, "#")
console.log("visible text:", typeof visibleText, visibleText, "#")
console.log("error:", err, "#")
}
}
/**
* Scrolls the element with the given id to the bottom.
* @param page Playwright Page object
* @param idElement The id of the scrollable element
*/
export async function scrollToBottomById(page: Page, idElement: string) {
await page.evaluate((id) => {
const el = document.getElementById(id);
if (el) {
el.scrollTop = el.scrollHeight;
}
}, idElement);
}
export async function scrollToTopById(page: Page, idElement: string) {
await page.evaluate((id) => {
const el = document.getElementById(id);
if (el) {
el.scrollTop = 0;
}
}, idElement);
}
export async function uploadFileWithPageAndFilepath(page: Page, filepath: string) {
console.log(`preparing uploading of file '${filepath}'!`)
await page.getByRole('link', { name: 'Save / Load' }).click();
await page.waitForTimeout(100)
const fileChooserPromise = page.waitForEvent('filechooser');
await page.getByRole('button', { name: '📁 Open File' }).click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(filepath);
await page.waitForTimeout(300)
console.log(`file '${filepath}' uploaded!!`)
}
export async function assertVisibleTextAfterNavigation(page: Page, idElement: string, expectedString: string, scrollTo: ScrollToPosition, idElementContentEditable: string = "gametext", projectName = "") {
// scroll to top gametext
if (scrollTo === "top") {
await scrollToTopById(page, idElementContentEditable);
} else if (scrollTo === "bottom") {
await scrollToBottomById(page, idElementContentEditable);
}
console.log("#")
if (projectName === "MobileChromeLandscape") {
let newIdEl = idElement.replace('-div', "")
await page.getByRole('link', { name: newIdEl }).click({timeout: 1000})
} else {
await page.getByLabel(idElement).click();
}
await page.waitForTimeout(200)
// assert visible gametext
if (projectName !== "MobileChromeLandscape") {
await expectVisibleTextWithWalker(page, idElementContentEditable, expectedString)
} else {
try {
await expect(page.locator(idElementContentEditable)).toHaveScreenshot()
} catch {
console.error("since the space is not much, only in case of MobileChromeLandscape let's skip this check... at least we tried =)")
}
}
await page.waitForTimeout(200)
}
export async function fillInputFieldWithString(page: Page, inputString: string, clickOrEnter: ClickOrEnter = "Enter"): Promise<void> {
await page.getByRole('searchbox', {name: 'Word Search Input'}).click();
await page.getByRole('searchbox', {name: 'Word Search Input'}).fill(inputString);
if (clickOrEnter === "click") {
await page.getByRole('button', { name: 'id-perform-wordsearch' }).click();
} else {
await page.getByRole('searchbox', {name: 'Word Search Input'}).press('Enter');
}
}
export async function initTest({page, workerInfo, filepath, targetUrl = 'http://localhost:8000/', setUi = true}: {
page: Page,
workerInfo: TestInfo,
filepath: string,
targetUrl?: string,
setUi?: boolean
}): Promise<string> {
const projectName = workerInfo.project.name
console.log("workerInfo:", workerInfo.project.name, "#")
// 1. Connect to the local web server page
if (targetUrl) await page.goto(targetUrl);
// 2. Activate the required UI mode (e.g., switch to classic or advanced UI)
console.log("setui:", setUi, "#")
if (setUi) await page.getByRole('button', { name: 'Set UI' }).click();
await openMobileMenu(page, "#found mobile button for global menu, open it to prepare json story upload!")
// 3. Upload a saved JSON story file to provide long text content for analysis
await uploadFileWithPageAndFilepath(page, filepath)
// activate wordsearch
await openMobileMenu(page, "#found mobile button for global menu, open it to toggle word search!")
await page.getByRole('link', { name: 'Settings' }).click();
await page.getByRole('checkbox', { name: 'WordSearch Tool' }).check();
await page.getByRole('button', { name: 'OK' }).click();
return projectName
}
export async function openMobileMenu(page: Page, msg: string) {
const mobileButtonGlobalMenu = page.getByRole('button', { name: 'Main Menu Options' })
if (await mobileButtonGlobalMenu.isVisible({timeout: 500})) {
await mobileButtonGlobalMenu.click();
await page.waitForTimeout(200)
console.log(msg)
}
}
export async function standardCheck(page: Page, projectName: string, expectedString: string, testName: string, click: boolean = true) {
// start as a normal test
if (click) await page.getByRole('button', { name: 'id-perform-wordsearch' }).click();
await page.waitForTimeout(200)
await expect(page.getByLabel('wordsearch_candidates_count')).toMatchAriaSnapshot(`- text: /1\\d\\d\\d result\\(s\\) found/`);
await expect(page.getByLabel('id-div-candidate-1-nth')).toMatchAriaSnapshot({name: `${testName}-0-${projectName}.txt`});
const wordsearch_results = page.getByLabel("wordsearch_results")
await expect(wordsearch_results).toMatchAriaSnapshot({name: `${testName}-1-${projectName}.txt`});
await page.waitForTimeout(200)
await page.getByLabel('id-div-candidate-1-nth').click();
await assertVisibleTextAfterNavigation(page, 'id-div-1-range-1-nth', expectedString, "bottom", "gametext", projectName);
await page.waitForTimeout(200)
} |