diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 0000000..e116ff4 --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,26 @@ +name: Playwright Tests + +on: push + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 18 + - name: Install dependencies + run: npm ci + - name: Start local website + run: npm start & + - name: Install Playwright Browsers + run: npx playwright install --with-deps + - name: Run Playwright tests + run: npx playwright test + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/.gitignore b/.gitignore index 4109d43..7b77452 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,9 @@ src/**/*.generated.json /cypress/screenshots *.log __diff_output__ -*.tgz \ No newline at end of file +*.tgz +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/.nvmrc b/.nvmrc index aabe6ec..3c03207 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -21 +18 diff --git a/.prettierignore b/.prettierignore index e3e008c..398e881 100644 --- a/.prettierignore +++ b/.prettierignore @@ -27,3 +27,4 @@ *.tgz *.zip .eslintcache +.nvmrc diff --git a/package-lock.json b/package-lock.json index 89a7276..406794c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ }, "devDependencies": { "@fortawesome/free-solid-svg-icons": "^6.6.0", + "@playwright/test": "^1.48.1", "@rollup/plugin-commonjs": "^28.0.1", "@rollup/plugin-node-resolve": "^15.3.0", "@rollup/plugin-replace": "^6.0.1", @@ -20,6 +21,7 @@ "@tsconfig/svelte": "^5.0.4", "@types/d3-array": "^3.2.1", "@types/d3-dsv": "^3.0.7", + "@types/node": "^22.7.8", "@types/uikit": "^3.14.5", "@typescript-eslint/eslint-plugin": "^8.11.0", "@typescript-eslint/parser": "^8.11.0", @@ -672,6 +674,21 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/@playwright/test": { + "version": "1.48.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.48.1.tgz", + "integrity": "sha512-s9RtWoxkOLmRJdw3oFvhFbs9OJS0BzrLUc8Hf6l2UdCNd1rqeEyD4BhCJkvzeEoD1FsK4mirsWwGerhVmYKtZg==", + "dev": true, + "dependencies": { + "playwright": "1.48.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@polka/url": { "version": "1.0.0-next.28", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz", @@ -855,10 +872,13 @@ "dev": true }, "node_modules/@types/node": { - "version": "18.7.13", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.13.tgz", - "integrity": "sha512-46yIhxSe5xEaJZXWdIBP7GU4HDTG8/eo0qd9atdiL+lFpA03y8KS+lkTN834TWJj5767GbWv4n/P6efyTFt1Dw==", - "dev": true + "version": "22.7.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.8.tgz", + "integrity": "sha512-a922jJy31vqR5sk+kAdIENJjHblqcZ4RmERviFsER4WJcEONqxKcjNOlk0q7OUfrF5sddT+vng070cdfMlrPLg==", + "dev": true, + "dependencies": { + "undici-types": "~6.19.2" + } }, "node_modules/@types/resolve": { "version": "1.20.2", @@ -2950,6 +2970,36 @@ "node": ">=0.10" } }, + "node_modules/playwright": { + "version": "1.48.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.48.1.tgz", + "integrity": "sha512-j8CiHW/V6HxmbntOfyB4+T/uk08tBy6ph0MpBXwuoofkSnLmlfdYNNkFTYD6ofzzlSqLA1fwH4vwvVFvJgLN0w==", + "dev": true, + "dependencies": { + "playwright-core": "1.48.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.48.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.1.tgz", + "integrity": "sha512-Yw/t4VAFX/bBr1OzwCuOMZkY1Cnb4z/doAFSwf4huqAGWmf9eMNjmK7NiOljCdLmxeRYcGPPmcDgU0zOlzP0YA==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/postcss": { "version": "8.4.47", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", @@ -4107,6 +4157,12 @@ "resolved": "https://registry.npmjs.org/uikit/-/uikit-3.21.13.tgz", "integrity": "sha512-Yc1VWCarqK8kMu9KCXfMH+1lQLZ0PxQRWML1ElJYt2KCwSNGxUVLJSDXcO4qtBR8asRAY+pHBtuM5iIQq0Odsg==" }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -4633,6 +4689,15 @@ "dev": true, "optional": true }, + "@playwright/test": { + "version": "1.48.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.48.1.tgz", + "integrity": "sha512-s9RtWoxkOLmRJdw3oFvhFbs9OJS0BzrLUc8Hf6l2UdCNd1rqeEyD4BhCJkvzeEoD1FsK4mirsWwGerhVmYKtZg==", + "dev": true, + "requires": { + "playwright": "1.48.1" + } + }, "@polka/url": { "version": "1.0.0-next.28", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz", @@ -4744,10 +4809,13 @@ "dev": true }, "@types/node": { - "version": "18.7.13", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.13.tgz", - "integrity": "sha512-46yIhxSe5xEaJZXWdIBP7GU4HDTG8/eo0qd9atdiL+lFpA03y8KS+lkTN834TWJj5767GbWv4n/P6efyTFt1Dw==", - "dev": true + "version": "22.7.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.8.tgz", + "integrity": "sha512-a922jJy31vqR5sk+kAdIENJjHblqcZ4RmERviFsER4WJcEONqxKcjNOlk0q7OUfrF5sddT+vng070cdfMlrPLg==", + "dev": true, + "requires": { + "undici-types": "~6.19.2" + } }, "@types/resolve": { "version": "1.20.2", @@ -6227,6 +6295,22 @@ "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", "dev": true }, + "playwright": { + "version": "1.48.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.48.1.tgz", + "integrity": "sha512-j8CiHW/V6HxmbntOfyB4+T/uk08tBy6ph0MpBXwuoofkSnLmlfdYNNkFTYD6ofzzlSqLA1fwH4vwvVFvJgLN0w==", + "dev": true, + "requires": { + "fsevents": "2.3.2", + "playwright-core": "1.48.1" + } + }, + "playwright-core": { + "version": "1.48.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.1.tgz", + "integrity": "sha512-Yw/t4VAFX/bBr1OzwCuOMZkY1Cnb4z/doAFSwf4huqAGWmf9eMNjmK7NiOljCdLmxeRYcGPPmcDgU0zOlzP0YA==", + "dev": true + }, "postcss": { "version": "8.4.47", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", @@ -6946,6 +7030,12 @@ "resolved": "https://registry.npmjs.org/uikit/-/uikit-3.21.13.tgz", "integrity": "sha512-Yc1VWCarqK8kMu9KCXfMH+1lQLZ0PxQRWML1ElJYt2KCwSNGxUVLJSDXcO4qtBR8asRAY+pHBtuM5iIQq0Odsg==" }, + "undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true + }, "uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", diff --git a/package.json b/package.json index 108e183..a3c6774 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ }, "devDependencies": { "@fortawesome/free-solid-svg-icons": "^6.6.0", + "@playwright/test": "^1.48.1", "@rollup/plugin-commonjs": "^28.0.1", "@rollup/plugin-node-resolve": "^15.3.0", "@rollup/plugin-replace": "^6.0.1", @@ -39,6 +40,7 @@ "@tsconfig/svelte": "^5.0.4", "@types/d3-array": "^3.2.1", "@types/d3-dsv": "^3.0.7", + "@types/node": "^22.7.8", "@types/uikit": "^3.14.5", "@typescript-eslint/eslint-plugin": "^8.11.0", "@typescript-eslint/parser": "^8.11.0", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..db2fc18 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,81 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// import path from 'path'; +// dotenv.config({ path: path.resolve(__dirname, '.env') }); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + // baseURL: 'http://127.0.0.1:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + /** Omit the extra browsers for rate limiting reasons + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + **/ + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // url: 'http://127.0.0.1:3000', + // reuseExistingServer: !process.env.CI, + // }, +}); diff --git a/src/api/EpiData.ts b/src/api/EpiData.ts index 1b3dfc5..142d2c5 100644 --- a/src/api/EpiData.ts +++ b/src/api/EpiData.ts @@ -118,6 +118,7 @@ export function loadDataSet( fixedParams: Record, userParams: Record, columns: string[], + api_key = '', columnRenamings: Record = {}, ): Promise { const duplicates = get(expandedDataGroups).filter((d) => d.title == title); @@ -131,7 +132,11 @@ export function loadDataSet( ) .then(() => null); } - const url = new URL(ENDPOINT + `/${endpoint}/`); + let url_string = ENDPOINT + `/${endpoint}/`; + if (api_key !== '') { + url_string += `?api_key=${api_key}`; + } + const url = new URL(url_string); const params = cleanParams(userParams); Object.entries(fixedParams).forEach(([key, value]) => { url.searchParams.set(key, String(value)); @@ -168,10 +173,14 @@ export function loadDataSet( }); } -export function fetchCOVIDcastMeta(): Promise< - { geo_type: string; signal: string; data_source: string; time_type?: string }[] -> { - const url = new URL(ENDPOINT + `/covidcast_meta/`); +export function fetchCOVIDcastMeta( + api_key: string, +): Promise<{ geo_type: string; signal: string; data_source: string; time_type?: string }[]> { + let url_string = ENDPOINT + `/covidcast_meta/`; + if (api_key !== '') { + url_string += `?api_key=${api_key}`; + } + const url = new URL(url_string); url.searchParams.set('format', 'json'); return fetchImpl<{ geo_type: string; signal: string; data_source: string; time_type?: string }[]>(url).catch( (error) => { @@ -190,8 +199,9 @@ export function importCDC({ locations, auth }: { locations: string; auth?: strin { epiweeks: epiRange(firstEpiWeek.cdc, currentEpiWeek), }, - { auth, locations }, + { locations }, ['total', 'num1', 'num2', 'num3', 'num4', 'num5', 'num6', 'num7', 'num8'], + auth, ); } @@ -201,12 +211,14 @@ export function importCOVIDcast({ geo_value, signal, time_type = 'day', + api_key, }: { data_source: string; signal: string; time_type?: string; geo_type: string; geo_value: string; + api_key: string; }): Promise { const title = `[API] COVIDcast: ${data_source}:${signal} (${geo_type}:${geo_value})`; return loadDataSet( @@ -221,6 +233,7 @@ export function importCOVIDcast({ }, { data_source, signal, time_type, geo_type, geo_value }, ['value', 'stderr', 'sample_size'], + api_key, ); } @@ -343,7 +356,7 @@ export function importFluView({ { epiweeks: epiRange(firstEpiWeek.fluview, currentEpiWeek), }, - { regions, issues, lag, auth }, + { regions, issues, lag }, [ 'wili', 'ili', @@ -357,6 +370,7 @@ export function importFluView({ 'num_age_4', 'num_age_5', ], + auth, { wili: '%wILI', ili: '%ILI', @@ -395,8 +409,9 @@ export function importGHT({ { epiweeks: epiRange(firstEpiWeek.ght, currentEpiWeek), }, - { auth, locations, query }, + { locations, query }, ['value'], + auth, ); } @@ -456,8 +471,9 @@ export function importQuidel({ auth, locations }: { auth: string; locations: str { epiweeks: epiRange(firstEpiWeek.quidel, currentEpiWeek), }, - { auth, locations }, + { locations }, ['value'], + auth, ); } export function importSensors({ @@ -478,8 +494,9 @@ export function importSensors({ { epiweeks: epiRange(firstEpiWeek.sensors, currentEpiWeek), }, - { auth, names, locations }, + { names, locations }, ['value'], + auth, ); } // twtr @@ -504,8 +521,9 @@ export function importTwitter({ : { epiweeks: epiRange(firstEpiWeek.twitter, currentEpiWeek), }, - { auth, locations, resolution }, + { locations, resolution }, ['num', 'total', 'percent'], + auth, ); } export function importWiki({ diff --git a/src/components/dialogs/dataSources/COVIDcast.svelte b/src/components/dialogs/dataSources/COVIDcast.svelte index 10e9699..074c881 100644 --- a/src/components/dialogs/dataSources/COVIDcast.svelte +++ b/src/components/dialogs/dataSources/COVIDcast.svelte @@ -7,10 +7,13 @@ export let id: string; + let api_key = ''; let data_source = ''; let signal = ''; let geo_type = ''; let geo_value = ''; + let form_key = ''; + let valid_key = true; let dataSources: (LabelValue & { signals: string[] })[] = []; let geoTypes: string[] = []; @@ -23,38 +26,77 @@ } } - onMount(() => { - fetchCOVIDcastMeta().then((res) => { - geoTypes = [...new Set(res.map((d) => d.geo_type))]; - const byDataSource = new Map(); - for (const row of res) { - const ds = byDataSource.get(row.data_source); - if (!ds) { - byDataSource.set(row.data_source, { - label: row.data_source, - value: row.data_source, - signals: [row.signal], - }); - } else if (!ds.signals.includes(row.signal)) { - ds.signals.push(row.signal); + // Helper function; delay invoking "fn" until "ms" milliseconds have passed + const debounce = (fn: Function, ms = 500) => { + let timeoutId: ReturnType; + return function (this: any, ...args: any[]) { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => fn.apply(this, args), ms); + }; + }; + + function fetchMetadata() { + fetchCOVIDcastMeta(form_key).then((res) => { + if (res.length == 0) { + valid_key = false; + } else { + valid_key = true; + api_key = form_key; // API key is valid -> use it to fetch data later on + geoTypes = [...new Set(res.map((d) => d.geo_type))]; + const byDataSource = new Map(); + for (const row of res) { + const ds = byDataSource.get(row.data_source); + if (!ds) { + byDataSource.set(row.data_source, { + label: row.data_source, + value: row.data_source, + signals: [row.signal], + }); + } else if (!ds.signals.includes(row.signal)) { + ds.signals.push(row.signal); + } } + byDataSource.forEach((entry) => { + entry.signals.sort(); + }); + dataSources = [...byDataSource.values()].sort((a, b) => a.value.localeCompare(b.value)); } - byDataSource.forEach((entry) => { - entry.signals.sort(); - }); - dataSources = [...byDataSource.values()].sort((a, b) => a.value.localeCompare(b.value)); }); + } + + onMount(() => { + fetchMetadata(); }); export function importDataSet() { - return fetchCOVIDcastMeta().then((res) => { + return fetchCOVIDcastMeta(api_key).then((res) => { const meta = res.filter((row) => row.data_source === data_source && row.signal === signal); const time_type = meta[0].time_type; - return importCOVIDcast({ data_source, signal, geo_type, geo_value, time_type }); + return importCOVIDcast({ data_source, signal, geo_type, geo_value, time_type, api_key }); }); } +
+ +
+ fetchMetadata(), 500)} + /> + {#if !valid_key} +
API key is invalid - ignoring
+ {/if} +
+
@@ -65,3 +107,10 @@ name="geo_values" placeholder="e.g., PA or 42003" /> + + diff --git a/tests/main.spec.ts b/tests/main.spec.ts new file mode 100644 index 0000000..b7babd5 --- /dev/null +++ b/tests/main.spec.ts @@ -0,0 +1,260 @@ +// Disabled due to working with JSON objects: +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import { test, expect } from '@playwright/test'; + +test('Webpage has title', async ({ page }) => { + await page.goto('http://localhost:8080/'); + + // Basic smoke test - expect the page to have a title + await expect(page).toHaveTitle(/EpiVis/); +}); + +test('Shepherd tour steps are successfully loaded', async ({ page }) => { + await page.goto('http://localhost:8080/'); + // Expect various Shepherd tour steps to be correctly loaded + await expect(page.getByLabel('Welcome to EpiVis')).toBeVisible(); + await page.getByRole('button', { name: 'Next' }).click(); + await expect(page.getByLabel('Main Chart')).toBeVisible(); + await page.getByRole('button', { name: 'Next' }).click(); + await expect(page.getByLabel('Mouse Panning')).toBeVisible(); + await page.getByRole('button', { name: 'Next' }).click(); + await expect(page.getByLabel('Mouse Cropping')).toBeVisible(); + await page.getByRole('button', { name: 'Next' }).click(); + await expect(page.getByLabel('Mouse Zooming')).toBeVisible(); + await page.getByRole('button', { name: 'Next' }).click(); + await expect(page.getByLabel('Chart Menu')).toBeVisible(); + await page.getByRole('button', { name: 'Next' }).click(); + await expect(page.getByLabel('Toggle between Navigation')).toBeVisible(); + await page.getByRole('button', { name: 'Next' }).click(); + await expect(page.getByLabel('Randomize Colors')).toBeVisible(); + await page.getByRole('button', { name: 'Next' }).click(); + await expect(page.getByLabel('Toggle Point Rendering')).toBeVisible(); + await page.getByRole('button', { name: 'Next' }).click(); + await expect(page.getByLabel('Dataset Scaling')).toBeVisible(); + await page.getByRole('button', { name: 'Next' }).click(); + await expect(page.getByLabel('Download Screenshot')).toBeVisible(); + await page.getByRole('button', { name: 'Next' }).click(); + await expect(page.getByLabel('Create Shareable Link')).toBeVisible(); + await page.getByRole('button', { name: 'Next' }).click(); + await expect(page.getByLabel('Data Browser')).toBeVisible(); + await page.getByRole('button', { name: 'Next' }).click(); + await expect(page.getByLabel('Load CSV File')).toBeVisible(); + await page.getByRole('button', { name: 'Next' }).click(); + await expect(page.getByLabel('Load Data from EpiData API')).toBeVisible(); + await page.getByRole('button', { name: 'Next' }).click(); + await expect(page.getByLabel('Draw a custom line (Advanced)')).toBeVisible(); + await page.getByRole('button', { name: 'Next' }).click(); + await expect(page.getByLabel('Derive via a kernel function')).toBeVisible(); + await page.getByRole('button', { name: 'Next' }).click(); + await expect(page.getByLabel('Finish')).toBeVisible(); + await page.getByRole('button', { name: 'Finish' }).click(); + // Also test finish button to close + await expect(page.getByLabel('Welcome to EpiVis')).toBeHidden(); + await page.locator('div:nth-child(5) > .uk-button').click(); + await expect(page.getByLabel('Welcome to EpiVis')).toBeVisible(); + await page.getByRole('button', { name: 'Cancel' }).click(); + await expect(page.getByLabel('Welcome to EpiVis')).toBeHidden(); +}); + +test('FluSurv Import is successfully loaded', async ({ page }) => { + await page.goto('http://localhost:8080/'); + await page.getByRole('button', { name: 'Cancel' }).click(); + await page.locator('side button').nth(1).click(); + // Input FluSurv Import + await page.getByLabel('FluSurv (source:').check(); + await page.getByRole('button', { name: 'Fetch Data' }).click(); + // Expect it to be loaded + await expect(page.getByRole('button', { name: '[API] FluSurv: Entire Network' })).toBeVisible(); +}); + +test('Google Flu Trends Import is successfully loaded', async ({ page }) => { + await page.goto('http://localhost:8080/'); + await page.getByRole('button', { name: 'Cancel' }).click(); + await page.locator('side button').nth(1).click(); + // Input FluSurv Import + await page.getByLabel('Google Flu Trends (source:').check(); + await page.getByRole('button', { name: 'Fetch Data' }).click(); + // Expect it to be loaded + await expect(page.getByRole('button', { name: '[API] GFT: U.S. National' })).toBeVisible(); +}); + +test('Wikipedia Import is successfully loaded', async ({ page }) => { + await page.goto('http://localhost:8080/'); + await page.getByRole('button', { name: 'Cancel' }).click(); + await page.locator('side button').nth(1).click(); + // Input FluSurv Import + await page.getByLabel('Wikipedia Access (source:').check(); + await page.getByRole('button', { name: 'Fetch Data' }).click(); + // Expect it to be loaded (30s timeout due to loading speed - typically loads within 10s though) + await expect(page.getByRole('button', { name: '[API] Wiki: amantadine, Daily' })).toBeVisible({ timeout: 30000 }); +}); + +test('NIDSS Influenza Import is successfully loaded', async ({ page }) => { + await page.goto('http://localhost:8080/'); + await page.getByRole('button', { name: 'Cancel' }).click(); + await page.locator('side button').nth(1).click(); + // Input Dengue Import + await page.getByLabel('NIDSS - Influenza (source:').check(); + await page.getByRole('button', { name: 'Fetch Data' }).click(); + // Expect it to be loaded + await expect(page.getByRole('button', { name: '[API] NIDSS-influenza: Taiwan National' })).toBeVisible(); +}); + +test('NIDSS Dengue Import is successfully loaded', async ({ page }) => { + await page.goto('http://localhost:8080/'); + await page.getByRole('button', { name: 'Cancel' }).click(); + await page.locator('side button').nth(1).click(); + // Input Dengue Import + await page.getByLabel('NIDSS - Dengue (source:').check(); + await page.getByRole('button', { name: 'Fetch Data' }).click(); + // Expect it to be loaded + await expect(page.getByRole('button', { name: '[API] NIDSS-Dengue: Taiwan National' })).toBeVisible(); +}); + +test('Nowcast Import is successfully loaded', async ({ page }) => { + await page.goto('http://localhost:8080/'); + await page.getByRole('button', { name: 'Cancel' }).click(); + await page.locator('side button').nth(1).click(); + // Input Dengue Import + await page.getByLabel('Nowcast (source:').check(); + await page.getByRole('button', { name: 'Fetch Data' }).click(); + // Expect it to be loaded + await expect(page.getByRole('button', { name: '[API] Delphi Nowcast: U.S. National' })).toBeVisible(); +}); + +test('COVIDCast Import is successfully loaded #1', async ({ page }) => { + await page.goto('http://localhost:8080/'); + await page.getByRole('button', { name: 'Cancel' }).click(); + await page.locator('side button').nth(1).click(); + // Input COVIDCast Import parameters + await page.getByLabel('Delphi COVIDcast (source:').check(); + await page.getByLabel('Data Source').selectOption('chng'); + await page.locator('select[name="signal"]').selectOption('7dav_inpatient_covid'); + await page.getByLabel('Geographic Type').selectOption('state'); + await page.getByPlaceholder('e.g., PA or').click(); + await page.getByPlaceholder('e.g., PA or').fill('pa'); + await page.getByRole('button', { name: 'Fetch Data' }).click(); + // Expect it to be loaded + await expect(page.getByRole('button', { name: '[API] COVIDcast: chng:' })).toBeVisible(); + // Also test selection and deselection + await expect(page.getByRole('button', { name: 'value' })).toBeVisible(); + await page.getByRole('button', { name: '[API] COVIDcast: chng:' }).click(); + await expect(page.getByRole('button', { name: 'value' })).toBeHidden(); + await page.getByRole('button', { name: '[API] COVIDcast: chng:' }).click(); + await expect(page.getByRole('button', { name: 'value' })).toBeVisible(); +}); + +test('COVIDCast Import is successfully loaded #2', async ({ page }) => { + await page.goto('http://localhost:8080/'); + await page.getByRole('button', { name: 'Cancel' }).click(); + await page.locator('side button').nth(1).click(); + // Input COVIDCast Import parameters + await page.getByLabel('Delphi COVIDcast (source:').check(); + await page.getByLabel('Data Source').selectOption('jhu-csse'); + await page.locator('select[name="signal"]').selectOption('confirmed_7dav_cumulative_num'); + await page.getByLabel('Geographic Type').selectOption('county'); + await page.getByPlaceholder('e.g., PA or').click(); + await page.getByPlaceholder('e.g., PA or').fill('42003'); + await page.getByRole('button', { name: 'Fetch Data' }).click(); + // Expect it to be loaded + await expect(page.getByRole('button', { name: '[API] COVIDcast: jhu-csse:' })).toBeVisible(); +}); + +test('COVIDCast Import is successfully loaded #3', async ({ page }) => { + await page.goto('http://localhost:8080/'); + await page.getByRole('button', { name: 'Cancel' }).click(); + await page.locator('side button').nth(1).click(); + // Input COVIDCast Import parameters + await page.getByLabel('Delphi COVIDcast (source:').check(); + await page.getByLabel('Data Source').selectOption('google-symptoms'); + await page.locator('select[name="signal"]').selectOption('ageusia_raw_search'); + await page.getByLabel('Geographic Type').selectOption('nation'); + await page.getByPlaceholder('e.g., PA or').click(); + await page.getByPlaceholder('e.g., PA or').fill('us'); + await page.getByRole('button', { name: 'Fetch Data' }).click(); + // Expect it to be loaded + await expect(page.getByRole('button', { name: '[API] COVIDcast: google-symptoms:' })).toBeVisible(); +}); + +test('COVID Hospitalization Import is successfully loaded', async ({ page }) => { + await page.goto('http://localhost:8080/'); + await page.getByRole('button', { name: 'Cancel' }).click(); + await page.locator('side button').nth(1).click(); + // Input Dengue Import + await page.getByLabel('COVID Hospitalization (source:').check(); + await page.getByRole('button', { name: 'Fetch Data' }).click(); + // Expect it to be loaded + await expect(page.getByRole('button', { name: '[API] COVID Hospitalization: AK' })).toBeVisible(); +}); + +test('Default dataset converts into a shareable link', async ({ page }) => { + await page.goto('http://localhost:8080/'); + await page.getByRole('button', { name: 'Cancel' }).click(); + // Expect initial dataset to be loaded + await expect(page.getByRole('button', { name: '%ILI' })).toBeVisible(); + // Get shareable link, convert to base64 + await page.locator('div:nth-child(4) > button:nth-child(2)').click(); + const shareableLink = await page.getByRole('textbox').inputValue(); + expect(shareableLink).toContain('http://localhost:8080/#'); + const base64 = shareableLink.split('#')[1]; + const decoded = Buffer.from(base64, 'base64').toString('binary'); + const json = JSON.parse(decoded); + // Expect elements from the initial dataset to be present + expect(json['datasets'][0]['title']).toEqual('%wILI'); + expect(json['datasets'][0]['params']).toEqual({ _endpoint: 'fluview', regions: 'nat' }); +}); + +test('Data is autofitted, but only when that mode is selected', async ({ page }) => { + function getViewport(shareableLink: string) { + const base64 = shareableLink.split('#')[1]; + const decoded = Buffer.from(base64, 'base64').toString('binary'); + const json = JSON.parse(decoded); + return json['chart']['viewport']; + } + + await page.goto('http://localhost:8080/'); + await page.getByRole('button', { name: 'Cancel' }).click(); + // Get initial viewport + await page.locator('div:nth-child(4) > button:nth-child(2)').click(); + const viewport1 = getViewport(await page.getByRole('textbox').inputValue()); + await page.getByLabel('Close').click(); + // Select num_patients, expect viewport to change + await page.getByRole('button', { name: 'num_patients' }).click(); + await page.waitForTimeout(3000); // viewport change animation + await page.locator('div:nth-child(4) > button:nth-child(2)').click(); + const viewport2 = getViewport(await page.getByRole('textbox').inputValue()); + expect(viewport1).not.toEqual(viewport2); + await page.getByLabel('Close').click(); + // Unselect autofit mode, expect viewport not to change + await page.locator('button:nth-child(2)').first().click(); + await page.getByRole('button', { name: 'num_patients' }).click(); + await page.locator('div:nth-child(4) > button:nth-child(2)').click(); + const viewport3 = getViewport(await page.getByRole('textbox').inputValue()); + expect(viewport3).toEqual(viewport2); + await page.getByLabel('Close').click(); +}); + +test('Colors are randomized', async ({ page }) => { + function getColor(shareableLink: string) { + const base64 = shareableLink.split('#')[1]; + const decoded = Buffer.from(base64, 'base64').toString('binary'); + const json = JSON.parse(decoded); + return json['datasets'][0]['color']; + } + + await page.goto('http://localhost:8080/'); + await page.getByRole('button', { name: 'Cancel' }).click(); + // Expect initial dataset to be loaded + await expect(page.getByRole('button', { name: '%ILI' })).toBeVisible(); + // Get initial color + await page.locator('div:nth-child(4) > button:nth-child(2)').click(); + const color1 = getColor(await page.getByRole('textbox').inputValue()); + await page.getByLabel('Close').click(); + // Randomize color, expect it to change + await page.locator('div:nth-child(2) > button:nth-child(1)').click(); + await page.locator('div:nth-child(4) > button:nth-child(2)').click(); + const color2 = getColor(await page.getByRole('textbox').inputValue()); + expect(color2).not.toEqual(color1); +}); diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json index fd23926..c545f19 100644 --- a/tsconfig.eslint.json +++ b/tsconfig.eslint.json @@ -1,5 +1,5 @@ { "extends": "./tsconfig.json", - "include": ["src/**/*", "types.d.ts", "*.js", ".eslintrc.js"], + "include": ["src/**/*", "tests/**/*", "types.d.ts", "*.js", ".eslintrc.js", "playwright.config.ts"], "exclude": ["node_modules/*", "__sapper__/*", "public/*"] }