diff --git a/.github/workflows/single_test_runner.yml b/.github/workflows/single_test_runner.yml index 1637d72..2d88897 100644 --- a/.github/workflows/single_test_runner.yml +++ b/.github/workflows/single_test_runner.yml @@ -30,6 +30,7 @@ on: - registrationSidebar.spec.ts - search.spec.ts - user.spec.ts + - projectFiles.spec.ts browser: description: 'Browser' required: true diff --git a/config/settings.ts b/config/settings.ts index b8c780d..c8aa2b2 100644 --- a/config/settings.ts +++ b/config/settings.ts @@ -137,6 +137,7 @@ export const ZOTERO_USER = env('ZOTERO_USER'); export const ZOTERO_PASSWORD = env('ZOTERO_PASSWORD'); export const DATAVERSE_URL = env('DATAVERSE_URL'); export const DATAVERSE_API_TOKEN = env('DATAVERSE_API_TOKEN'); +export const ADDONS_TEST_PROJECT_TITLE = 'OSF Test Project for Addons'; // Populated at runtime by the `waffledPages` fixture (see src/fixtures/index.ts), // mirroring `settings.EMBER_PAGES` being set dynamically in the old conftest.py. diff --git a/package-lock.json b/package-lock.json index 7f9e0c2..b915914 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ }, "devDependencies": { "@playwright/test": "^1.48.0", - "@types/node": "^22.7.0", + "@types/node": "^22.20.1", "cross-env": "^7.0.3", "typescript": "^5.6.0" } diff --git a/package.json b/package.json index a17dd45..025814a 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ }, "devDependencies": { "@playwright/test": "^1.48.0", - "@types/node": "^22.7.0", + "@types/node": "^22.20.1", "cross-env": "^7.0.3", "typescript": "^5.6.0" }, diff --git a/playwright.config.ts b/playwright.config.ts index 0644c99..fe5f8f7 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -61,5 +61,6 @@ export default defineConfig({ name: 'webkit', use: { ...devices['Desktop Safari'] }, }, + ], }); diff --git a/src/api/osfApi.ts b/src/api/osfApi.ts index ba69af8..ddc4a15 100644 --- a/src/api/osfApi.ts +++ b/src/api/osfApi.ts @@ -848,3 +848,24 @@ export async function getRegistrationVolKey(registrationGuid: string): Promise { + try { + const url = `/v2/nodes/?filter[title]=${encodeURIComponent(title)}`; + + const response = await session.get(url); + + const data = response.data; + + if (data && data.length > 0) { + return data[0].id; + } + + return undefined; + } catch (error) { + console.error(`Failed to get node by title "${title}":`, error); + throw error; + } +} diff --git a/src/fixtures/index.ts b/src/fixtures/index.ts index 47966a4..37c7208 100644 --- a/src/fixtures/index.ts +++ b/src/fixtures/index.ts @@ -21,17 +21,24 @@ export interface ProjectPageStub { guid: string; } -type Fixtures = { +type WorkerFixtures = { session: OsfSession; checkCredentials: void; - fake: Faker; waffledPages: void; +}; + +type Fixtures = { + //session: OsfSession; + //checkCredentials: void; + fake: Faker; + //waffledPages: void; hideFooterSlideIn: void; hideMetadataFeaturePopover: void; defaultLogout: void; mustBeLoggedIn: void; userLoggedIn: boolean; logInIfNotAlready: void; + mustBeLoggedInAsUserOne: void; mustBeLoggedInAsUserTwo: void; mustBeLoggedInAsProfileUser: void; loginAsUserWithRegistrations: void; @@ -54,6 +61,7 @@ type Fixtures = { defaultProjectWithTags: osfApi.OsfProject; publicLinkProject: osfApi.OsfProject; defaultProjectWithAllMetadata: osfApi.OsfProject; + defaultAddonsProject: osfApi.OsfProject; }; function getSessionCookieName(): string { @@ -63,12 +71,12 @@ function getSessionCookieName(): string { return `osf_${match ? match[1] : settings.DOMAIN}`; } -export const test = base.extend({ - session: async ({}, use) => { +export const test = base.extend({ + session: [async ({}, use) => { const session = await createSession(); await use(session); await session.dispose(); - }, + }, { scope: 'worker' }], // Port of `check_credentials` (autouse). `pytest.exit` aborted the whole session on // failure; here we fail fast with a clear error on the current test instead. @@ -81,9 +89,10 @@ export const test = base.extend({ } await use(); }, - { auto: true }, + { auto: true, scope: 'worker' }, ], + fake: async ({}, use) => { await use(faker); }, @@ -93,7 +102,7 @@ export const test = base.extend({ settings.runtime.emberPages = await osfApi.waffledPages(session); await use(); }, - { auto: true }, + { auto: true, scope: 'worker' }, ], hideFooterSlideIn: async ({ page }, use) => { @@ -140,6 +149,12 @@ export const test = base.extend({ await use(); }, + mustBeLoggedInAsUserOne: async ({ page }, use) => { + await safeLogin(page, settings.USER_ONE, settings.USER_ONE_PASSWORD); + await acceptCookies(page); + await use(); + }, + mustBeLoggedInAsUserTwo: async ({ page }, use) => { await safeLogin(page, settings.USER_TWO, settings.USER_TWO_PASSWORD); await acceptCookies(page); @@ -367,6 +382,29 @@ export const test = base.extend({ await use(project); await project.delete(); }, + + defaultAddonsProject: async ({ session }, use) => { + /** + * Creates a new project through the api and returns it. Deletes the project at the end + * of the test run. If PREFERRED_NODE is set, returns the APIDetail of preferred node. + */ + let node: osfApi.OsfProject; + + if (settings.PREFERRED_NODE) { + node = await osfApi.getNode(session); + } else { + const nodeId = await osfApi.getNodeIdByTitle(session, settings.ADDONS_TEST_PROJECT_TITLE); + if (!nodeId) { + throw new Error('Could not find node with title "OSF Test Project for Addons"'); + } + node = await osfApi.getNode(session, nodeId); + } + + await use(node); + // teardown — the Python docstring says "Deletes the project at the end of the test run," + // but the code shown doesn't actually perform a delete. Add it here if that's expected: + // await osfApi.deleteNode(session, node.id); + }, }); export { expect }; diff --git a/src/pages/FilesPage.ts b/src/pages/FilesPage.ts new file mode 100644 index 0000000..fa7195d --- /dev/null +++ b/src/pages/FilesPage.ts @@ -0,0 +1,126 @@ +import { Page, Locator, expect } from '@playwright/test'; +import { GuidBasePage } from './GuidBasePage'; +import * as settings from '../../config/settings'; +import { present, clickExpectingPopup, waitUntilPageReady, hereThenGone } from '../utils'; + +export class FilesPage extends GuidBasePage { + static baseUrl = '{guid}/files/{provider}'; + addonProvider: string; + // Locators + + readonly addFileFolderButton: Locator; + readonly fileSelectedText: Locator; + readonly fileListMoveButton: Locator; + readonly fileListCopyButton: Locator; + readonly fileListDeleteButton: Locator; + readonly leftnavOsfstorageLink: Locator; + readonly selectAddon: Locator; + readonly addonsList: Locator; + readonly fileInput: Locator; + readonly searchInput: Locator; + readonly searchResults: Locator; + readonly sortDropdown: Locator; + readonly selectListContainer: Locator; + + + constructor( + page: Page, + verify: boolean = false, + guid: string = '', + domain: string = settings.OSF_HOME, + addonProvider: string = 'osfstorage', + ) { + super(page, verify, guid, domain); + this.addonProvider = addonProvider; + + // Prefer resilient, role-based or test-id selectors over fragile long utility classes + this.addFileFolderButton = page.locator('button.p-button-success'); + this.fileSelectedText = page.locator('span.mr-2'); + this.fileListMoveButton = page.locator('button.p-button-outlined:not(.p-button-success):not(.p-button-danger)'); + this.fileListCopyButton = page.locator('button.p-button-success'); + this.fileListDeleteButton = page.locator('button.p-button-danger'); + + this.leftnavOsfstorageLink = page.locator('[data-test-files-provider-link="osfstorage"]'); + this.selectAddon = page.getByRole('button', { name: 'dropdown trigger' }); + this.addonsList = page.locator('li.p-select-option'); + this.fileInput = page.locator('input[type="file"]'); + this.searchInput = page.getByPlaceholder('Search your files'); + this.searchResults = page.locator('div.table-cell.flex.align-items-center'); + this.sortDropdown = page.locator('div.p-select-dropdown').nth(1); + this.selectListContainer = page.locator('div.p-select-list-container'); + } + + + get url(): string { + return `${this.domain}/${FilesPage.baseUrl + .replace('{guid}', this.guid) + .replace('{provider}', this.addonProvider)}`; + } + + get identity(): Locator { + return this.page.locator('[data-test-file-search]'); + } + + async reload() { + await this.page.reload(); + } + + get downloadButton(): Locator { + return this.page.locator('button[aria-label="Download"]'); + } + + async selectFromAddonList(selection: string): Promise { + // Replaces explicit loops with direct text-based locators + await this.selectAddon.nth(0).click(); + const targetAddon = this.addonsList.filter({ + hasText: new RegExp(`^\\s*${selection}\\s*$`, 'i'), + }); + await targetAddon.click(); + } + + async selectSortFromList(sortName: string): Promise { + // Select the second dropdown directly + await this.selectAddon.nth(1).click(); + + const sortOption = this.page + .locator('div.p-select-list-container li') + .filter({ hasText: new RegExp(`^\\s*${sortName}\\s*$`) }); + + await sortOption.click(); + } + + async clickOnButton(buttonName: string): Promise { + await this.page.getByRole('button', { name: buttonName }).click(); + } + + async clickOnFolderLink(folderName: string, parentRow?: Locator): Promise { + const scope = parentRow || this.page; + await scope.locator('span', { hasText: folderName }).click(); + } + + async selectFromSearchResults(fileName: string): Promise { + // Wait for the filtered files API response to complete before trusting the DOM + await this.page.waitForResponse( + (response) => response.url().includes(`filter%5Bname%5D=${encodeURIComponent(fileName)}`) && response.status() === 200, + { timeout: 10000 } + ).catch(() => undefined); // don't hard-fail if the URL pattern doesn't match exactly — fall through to DOM wait below + + const matchingResult = this.page.locator('div.files-table-row').filter({ + has: this.page.locator(`text="${fileName}"`), // exact text match, not substring + }).first(); + + try { + await matchingResult.waitFor({ state: 'visible', timeout: 10000 }); + return matchingResult; + } catch { + return null; + } + } + + async retrieveSearchResults(targetString: string): Promise { + const matchingResults = this.searchResults.filter({ hasText: targetString }); + return matchingResults.all(); + } + + +} diff --git a/src/pages/GuidBasePage.ts b/src/pages/GuidBasePage.ts new file mode 100644 index 0000000..bc0e6ca --- /dev/null +++ b/src/pages/GuidBasePage.ts @@ -0,0 +1,42 @@ +// guid-base-page.ts +import { Page, Locator, expect } from '@playwright/test'; +import { BasePage } from './BasePage'; +import * as settings from '../../config/settings'; + + +export abstract class GuidBasePage extends BasePage { + static baseUrl: string = ''; + guid: string; + domain: string; + + constructor( + page: Page, + verify: boolean = false, + guid: string = '', + domain: string = settings.OSF_HOME + ) { + super(page); + this.domain = domain; + this.guid = guid; + } + + get url(): string { + const baseUrl = (this.constructor as typeof GuidBasePage).baseUrl; + if (baseUrl.includes('{guid}')) { + return `${this.domain}/${baseUrl.replace('{guid}', this.guid)}`; + } else { + throw new Error('No {guid} placeholder in base_url specified.'); + } + } + + // guid-base-page.ts + async gotoGuid(): Promise { + await this.page.goto(this.url); + try { + await this.page.getByText('Accept cookies').click({ timeout: 3000 }); + } catch { + // banner not present, continue + } + return this; + } +} \ No newline at end of file diff --git a/tests/projectFiles.spec.ts b/tests/projectFiles.spec.ts new file mode 100644 index 0000000..cd2ea8b --- /dev/null +++ b/tests/projectFiles.spec.ts @@ -0,0 +1,394 @@ +import { test as base, expect } from '../src/fixtures'; +import { Page, Locator } from '@playwright/test'; +import { FilesPage } from '../src/pages/FilesPage'; +import * as osfApi from '../src/api/osfApi'; +import * as settings from '../config/settings'; +import { present, clickExpectingPopup } from '../src/utils'; +import fs from 'fs'; +import * as path from 'path'; +import * as os from 'os'; + +const test = base.extend<{ + filesPage: FilesPage; + defaultAddonsProject: osfApi.OsfProject; +}>({ + filesPage: async ({ page, defaultAddonsProject, mustBeLoggedIn }, use) => { + void mustBeLoggedIn; // ensure login fixture resolves before this runs; + + const filesPage = new FilesPage( + page, + false, + defaultAddonsProject.id, + settings.OSF_HOME, + 'osfstorage' // or whatever default/appropriate addon provider + ); + await filesPage.gotoGuid(); + await use(filesPage); + }, + +}); + +async function findFileBySearch(filesPage: FilesPage, targetFileName: string) { + // Search for target file + await filesPage.searchInput.clear(); + await filesPage.searchInput.fill(targetFileName); + const row = await filesPage.selectFromSearchResults(targetFileName); + return row; +} + +async function findFolderBySearch(filesPage: FilesPage, targetFolderName: string) { + // Search for target file + await filesPage.searchInput.clear(); + await filesPage.searchInput.fill(targetFolderName); + const row = await filesPage.selectFromSearchResults(targetFolderName); + return row; +} + +async function verifyFileDownload( + page: Page, + filesPage: FilesPage, + fileName: string, + provider: string +) { + /** + * Helper function to verify the file download functionality on the Project Files page. + */ + + // If running on local machine, first check if the download file already exists + // in the Downloads folder. If so then delete the old copy before attempting to + // download a new one. + const filePath = path.join(os.homedir(), 'Downloads', fileName); + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + } + + const rowPromise = findFileBySearch(filesPage, fileName); + const row = await rowPromise; + if (!row) { + throw new Error(`Could not find row for file: ${fileName}`); + } + + // Click the File Action menu button at the far right side of the row to show the + // menu options. Then click the Download option from this menu. + const menuButton = row.locator( + 'button.p-ripple.p-button.p-button-contrast.p-button-icon-only.p-button-raised.p-button-sm.p-button-text.p-component' + ); + await menuButton.click(); + + + // The menu overlay renders outside the row (PrimeNG overlay), so scope to page, not row + const downloadButton = page.locator('li#download'); // overlay renders via appendto="body", outside row + const downloadPromise = page.waitForEvent('download'); + + await downloadButton.click(); + const download = await downloadPromise; + + // Save it explicitly to a known path (or just verify via the Download object's own API) + const downloadPath = path.join(os.homedir(), 'Downloads', fileName); + await download.saveAs(downloadPath); + + await filesPage.reload(); + + if (provider !== 'osfstorage') { + //await filesPage.selectAddon.click(); + await filesPage.selectFromAddonList(provider); + } + + const currentDate = new Date(); + expect(fs.existsSync(filePath)).toBeTruthy(); + + const stats = fs.statSync(filePath); + const fileModDate = new Date(stats.mtime); + expect(fileModDate.toDateString()).toBe(currentDate.toDateString()); +} + +type SortCase = { + title: string; + sortOption: string; + /** File name prefixes, uploaded in this order (order matters for the date tests). */ + prefixes: string[]; + column: 'name' | 'modified'; + order: 'asc' | 'desc'; + }; + + const SORT_CASES: SortCase[] = [ + { + title: 'sort name A to Z', + sortOption: 'Name: A-Z', + prefixes: ['1', 'ZZ', '2'], + column: 'name', + order: 'asc', + }, + { + title: 'sort name Z to A', + sortOption: 'Name: Z-A', + prefixes: ['1', '2', 'ZZ'], + column: 'name', + order: 'desc', + }, + { + // NOTE: kept exactly as the Selenium test: the name says "descending", + // but it selects oldest -> newest and asserts ascending order. + title: 'sort modified date descending', + sortOption: 'Last modified: oldest to newest', + prefixes: ['Oldest', 'Newest'], + column: 'modified', + order: 'asc', + }, + { + // NOTE: same naming mismatch as above, in reverse. + title: 'sort modified date ascending', + sortOption: 'Last modified: newest to oldest', + prefixes: ['Oldest', 'Newest'], + column: 'modified', + order: 'desc', + }, + ]; + +const MONTHS = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec']; + + +function parseOsfDate(text: string): number { + const m = text.trim().match(/^([A-Za-z]{3}) (\d{1,2}), (\d{2}) (\d{1,2}):(\d{2}) ([AP]M)$/i); + if (!m) throw new Error(`Unexpected date format: "${text}"`); + const [, mon, day, yy, hh, mm, ampm] = m; + let hour = Number(hh) % 12; + if (ampm.toUpperCase() === 'PM') hour += 12; + return new Date(2000 + Number(yy), MONTHS.indexOf(mon.toLowerCase()), Number(day), hour, Number(mm)).getTime(); +} + +function isSorted(keys: (string | number)[], order: SortCase['order']): boolean { + return keys.every((key, i) => i === 0 || (order === 'asc' ? keys[i - 1] <= key : keys[i - 1] >= key)); +} + +/** Reads the column as sortable keys (lowercased names, like key=str.lower, or timestamps). */ +async function readSortKeys(page: Page, column: SortCase['column']): Promise<(string | number)[]> { + if (column === 'name') { + const texts = await page.locator('span.entry-title').allInnerTexts(); + return texts.map((t) => t.trim().toLowerCase()); + } + const texts = await page.locator('xpath=//div[@class="files-table-cell"][3]').allInnerTexts(); + return texts.map(parseOsfDate); +} + +// Example parameterized test replacing python's provider fixture +const UNSUPPORTED_PROVIDERS = ['bitbucket', 'dataverse', 'figshare', 'gitlab', 'onedrive', 'googledrive']; +const providers = ['osfstorage', 's3', 'box', 'bitbucket', 'dataverse', 'dropbox', 'figshare', 'github', 'gitlab', 'googledrive', 'onedrive', 'owncloud']; +//const providers = ['osfstorage']; + +test.describe('Project Files Page', { tag: '@core' }, () => { + test.beforeEach(() => { + test.skip(settings.PRODUCTION, 'Test should not run on production'); + }); + + for (const provider of providers) { + test(`Download a single file from ${provider}`, async ({ page, filesPage }) => { + test.skip(UNSUPPORTED_PROVIDERS.includes(provider), 'Functionality not supported'); + // Substitute with your actual project ID or fixture login + const currentBrowser = page.context().browser()?.browserType().name(); + const currentBrowserName: string = currentBrowser === 'chromium' ? 'chrome' : (currentBrowser ?? 'unknown'); + const fileName = 'download_' + currentBrowserName + '_' + provider + '.txt'; + + if (provider !== 'osfstorage') { + //await filesPage.selectAddon.click(); + await filesPage.selectFromAddonList(provider); + } + + // Trigger and verify download + + await verifyFileDownload(page, filesPage, fileName, provider) + + }); + } + + for (const provider of providers) { + test(`Download a folder from ${provider}`, async ({ page, filesPage }) => { + test.skip(UNSUPPORTED_PROVIDERS.includes(provider), 'Functionality not supported'); + // Substitute with your actual project ID or fixture login + const currentBrowser = page.context().browser()?.browserType().name(); + const currentBrowserName: string = currentBrowser === 'chromium' ? 'chrome' : (currentBrowser ?? 'unknown'); + const folderName = 'download_' + currentBrowserName + '_' + provider ; + + if (provider !== 'osfstorage') { + //await filesPage.selectAddon.click(); + await filesPage.selectFromAddonList(provider); + } + + // Open the folder (search + click lives in the page object) + const filePath = path.join(os.homedir(), 'Downloads', folderName); + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + } + + const rowPromise = findFolderBySearch(filesPage, folderName); + const row = await rowPromise; + if (!row) { + throw new Error(`Could not find row for file: ${folderName}`); + } + + // Click the File Action menu button at the far right side of the row to show the + // menu options. Then click the Download option from this menu. + const menuButton = row.locator( + 'button.p-ripple.p-button.p-button-contrast.p-button-icon-only.p-button-raised.p-button-sm.p-button-text.p-component' + ); + await menuButton.click(); + + // The menu overlay renders outside the row (PrimeNG overlay), so scope to page, not row + const downloadButton = page.locator('li#download'); // overlay renders via appendto="body", outside row + const downloadPromise = page.waitForEvent('download'); + + await downloadButton.click(); + const download = await downloadPromise; + + // Save it explicitly to a known path (or just verify via the Download object's own API) + const downloadFolderName = folderName+'.zip' + const downloadPath = path.join(os.homedir(), 'Downloads', downloadFolderName); + await download.saveAs(downloadPath); + + await filesPage.reload(); + + if (provider !== 'osfstorage') { + //await filesPage.selectAddon.click(); + await filesPage.selectFromAddonList(provider); + } + + const currentDate = new Date(); + expect(fs.existsSync(downloadPath)).toBeTruthy(); + + const stats = fs.statSync(downloadPath); + const fileModDate = new Date(stats.mtime); + expect(fileModDate.toDateString()).toBe(currentDate.toDateString()); + + + }); + } + +}) + +for (const provider of providers) { + test(`Download as a zip from ${provider}`, async ({ page, filesPage }) => { + test.skip(UNSUPPORTED_PROVIDERS.includes(provider), 'Functionality not supported'); + // Substitute with your actual project ID or fixture login + const currentBrowser = page.context().browser()?.browserType().name(); + const currentBrowserName: string = currentBrowser === 'chromium' ? 'chrome' : (currentBrowser ?? 'unknown'); + const folderName = 'download_' + currentBrowserName + '_' + provider; + + if (provider !== 'osfstorage') { + //await filesPage.selectAddon.click(); + await filesPage.selectFromAddonList(provider); + } + + // Open the folder (search + click lives in the page object) + const filePath = path.join(os.homedir(), 'Downloads', folderName); + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + } + + const rowPromise = findFolderBySearch(filesPage, folderName); + const row = await rowPromise; + if (!row) { + throw new Error(`Could not find row for file: ${folderName}`); + } + + // Click the Folder link to navagate inside the folder + await filesPage.clickOnFolderLink(folderName, row); + + + // The menu overlay renders outside the row (PrimeNG overlay), so scope to page, not row + //const downloadButton = page.locator('li#download'); // overlay renders via appendto="body", outside row + const downloadPromise = page.waitForEvent('download'); + + await filesPage.clickOnButton('Download As Zip') + const download = await downloadPromise; + + // Save it explicitly to a known path (or just verify via the Download object's own API) + const downloadFolderName = folderName + '.zip' + const downloadPath = path.join(os.homedir(), 'Downloads', downloadFolderName); + await download.saveAs(downloadPath); + + await filesPage.reload(); + + if (provider !== 'osfstorage') { + //await filesPage.selectAddon.click(); + await filesPage.selectFromAddonList(provider); + } + + const currentDate = new Date(); + expect(fs.existsSync(downloadPath)).toBeTruthy(); + + const stats = fs.statSync(downloadPath); + const fileModDate = new Date(stats.mtime); + expect(fileModDate.toDateString()).toBe(currentDate.toDateString()); + + + }); +} + +for (const provider of providers) { + test(`Top level Download as zip from ${provider}`, async ({ page, filesPage, defaultAddonsProject }) => { + test.skip(UNSUPPORTED_PROVIDERS.includes(provider), 'Functionality not supported'); + // Substitute with your actual project ID or fixture login + //const node_id = defaultAddonsProject.id; + const currentBrowser = page.context().browser()?.browserType().name(); + const currentBrowserName: string = currentBrowser === 'chromium' ? 'chrome' : (currentBrowser ?? 'unknown'); + const folderName = provider + '-archive.zip' + + if (provider !== 'osfstorage') { + await filesPage.selectFromAddonList(provider); + } + + const downloadPromise = page.waitForEvent('download'); + + await filesPage.clickOnButton('Download As Zip') + const download = await downloadPromise; + + // Save it explicitly to a known path (or just verify via the Download object's own API) + //const downloadFolderName = folderName + '.zip' + const downloadPath = path.join(os.homedir(), 'Downloads', folderName); + await download.saveAs(downloadPath); + + await filesPage.reload(); + + if (provider !== 'osfstorage') { + //await filesPage.selectAddon.click(); + await filesPage.selectFromAddonList(provider); + } + + const currentDate = new Date(); + expect(fs.existsSync(downloadPath)).toBeTruthy(); + + const stats = fs.statSync(downloadPath); + const fileModDate = new Date(stats.mtime); + expect(fileModDate.toDateString()).toBe(currentDate.toDateString()); + + }); +} + +test.describe('Files page sort', () => { + for (const provider of providers) { + for (const sortCase of SORT_CASES) { + test(`${sortCase.title} - ${provider}`, async ({ page, filesPage, session, browserName }) => { + test.skip(UNSUPPORTED_PROVIDERS.includes(provider), 'Functionality not supported'); + + // Upload each test file, in order + const fileNames = sortCase.prefixes.map((p) => `${p} AQA_${provider}.txt`); + + if (provider !== 'osfstorage') { + //await filesPage.selectAddon.click(); + await filesPage.selectFromAddonList(provider); + } + + await filesPage.selectSortFromList(sortCase.sortOption); + + // Replaces time.sleep + one-shot assert: retry until the list re-renders sorted + await expect + .poll(async () => { + const keys = await readSortKeys(page, sortCase.column); + return isSorted(keys, sortCase.order) ? 'sorted' : `not sorted: ${keys.join(' | ')}`; + }) + .toBe('sorted'); + }); + } + } + +}); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index ac6c43b..420e30a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,8 @@ "compilerOptions": { "target": "ES2022", "module": "commonjs", - "moduleResolution": "node", + "moduleResolution": "node10", + "ignoreDeprecations": "6.0", "lib": ["ES2022", "DOM"], "strict": true, "esModuleInterop": true,