|
|
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") { |
|
|
|
|
|
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) { |
|
|
|
|
|
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"); |
|
|
|
|
|
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) { |
|
|
|
|
|
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> { |
|
|
|
|
|
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 } |
|
|
); |
|
|
|
|
|
|
|
|
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); |
|
|
|
|
|
return visible.includes(expected); |
|
|
}, |
|
|
{ idElement, expected: expectedString }, |
|
|
{ timeout } |
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
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) { |
|
|
|
|
|
|
|
|
console.error(`Scroll state for #${idElement}:`, scrolled); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export async function expectOnlyVisibleTextInElement( |
|
|
page: Page, |
|
|
idElement: string, |
|
|
expectedVisible: string |
|
|
) { |
|
|
|
|
|
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}!`); |
|
|
|
|
|
try { |
|
|
expect(visibleText).toContain(expectedVisible); |
|
|
} catch (err) { |
|
|
console.log("expectedVisible:", typeof expectedVisible, expectedVisible, "#") |
|
|
console.log("visible text:", typeof visibleText, visibleText, "#") |
|
|
console.log("error:", err, "#") |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 = "") { |
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
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, "#") |
|
|
|
|
|
if (targetUrl) await page.goto(targetUrl); |
|
|
|
|
|
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!") |
|
|
|
|
|
await uploadFileWithPageAndFilepath(page, filepath) |
|
|
|
|
|
|
|
|
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) { |
|
|
|
|
|
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) |
|
|
} |