diff --git a/.github/workflows/docker_latest.yml b/.github/workflows/docker_latest.yml index 6140fc76..5b79ef03 100644 --- a/.github/workflows/docker_latest.yml +++ b/.github/workflows/docker_latest.yml @@ -18,7 +18,7 @@ jobs: strategy: matrix: python-version: [3.9] - node-version: [14.x] + node-version: [18.x] steps: - name: Check out the repo uses: actions/checkout@v2 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 98db1d33..28a244fb 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -18,7 +18,7 @@ jobs: fail-fast: false matrix: python-version: [3.9] - node-version: [14.x] + node-version: [18.x] tests: - ':lint:python:client:common:smoke:' - ':server-1-of-2:' @@ -30,10 +30,10 @@ jobs: - ':nbrowser-^[^A-S]:' include: - tests: ':lint:python:client:common:smoke:' - node-version: 14.x + node-version: 18.x python-version: '3.10' - tests: ':lint:python:client:common:smoke:' - node-version: 14.x + node-version: 18.x python-version: '3.11' steps: - uses: actions/checkout@v3 @@ -72,6 +72,10 @@ jobs: - name: Build Node.js code run: yarn run build:prod + - name: Install chromedriver + if: contains(matrix.tests, ':nbrowser-') || contains(matrix.tests, ':smoke:') + run: ./node_modules/selenium-webdriver/bin/linux/selenium-manager --driver chromedriver + - name: Run smoke test if: contains(matrix.tests, ':smoke:') run: VERBOSE=1 DEBUG=1 MOCHA_WEBDRIVER_HEADLESS=1 yarn run test:smoke diff --git a/.gitignore b/.gitignore index cc60d2eb..1d2fa534 100644 --- a/.gitignore +++ b/.gitignore @@ -77,5 +77,6 @@ jspm_packages/ # Test timings.txt xunit.xml +.clipboard.lock **/_build diff --git a/.nvmrc b/.nvmrc index 28515227..4a1f488b 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -14.20.1 +18.17.1 diff --git a/Dockerfile b/Dockerfile index 17415c0f..1b4002e2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,7 +10,7 @@ FROM scratch as ext ## Javascript build stage ################################################################################ -FROM node:14-buster as builder +FROM node:18-buster as builder # Install all node dependencies. WORKDIR /grist @@ -67,7 +67,7 @@ FROM gristlabs/gvisor-unprivileged:buster as sandbox ################################################################################ # Now, start preparing final image. -FROM node:14-buster-slim +FROM node:18-buster-slim # Install libexpat1, libsqlite3-0 for python3 library binary dependencies. # Install pgrep for managing gvisor processes. diff --git a/app/client/ui/CustomSectionConfig.ts b/app/client/ui/CustomSectionConfig.ts index 538562f5..3c6b7404 100644 --- a/app/client/ui/CustomSectionConfig.ts +++ b/app/client/ui/CustomSectionConfig.ts @@ -628,6 +628,8 @@ export class CustomSectionConfig extends Disposable { protected async _getWidgets() { const api = this._gristDoc.app.topAppModel.api; const widgets = await api.getWidgets(); + if (this.isDisposed()) { return; } + // Request for rest of the widgets. if (this._canSelect) { // From the start we will provide single widget definition diff --git a/app/plugin/grist-plugin-api.ts b/app/plugin/grist-plugin-api.ts index d9585881..218eabd1 100644 --- a/app/plugin/grist-plugin-api.ts +++ b/app/plugin/grist-plugin-api.ts @@ -243,6 +243,15 @@ async function getMappingsIfChanged(data: any): Promise { return _mappingsCache ? JSON.parse(JSON.stringify(_mappingsCache)) : null; } +/** + * Used by tests to wait for all pending requests to settle. + * + * TODO: currently only waits for requests for mappings. + */ +export async function testWaitForPendingRequests() { + return await _activeRefreshReq; +} + /** * Renames columns in the result using columns mapping configuration passed in ready method. * Returns null if not all required columns were mapped or not widget doesn't support diff --git a/app/server/lib/FlexServer.ts b/app/server/lib/FlexServer.ts index 73eb084c..f228a6d8 100644 --- a/app/server/lib/FlexServer.ts +++ b/app/server/lib/FlexServer.ts @@ -68,7 +68,6 @@ import {addUploadRoute} from 'app/server/lib/uploads'; import {buildWidgetRepository, IWidgetRepository} from 'app/server/lib/WidgetRepository'; import {setupLocale} from 'app/server/localization'; import axios from 'axios'; -import * as bodyParser from 'body-parser'; import * as cookie from 'cookie'; import express from 'express'; import * as fse from 'fs-extra'; @@ -789,7 +788,7 @@ export class FlexServer implements GristServer { public addJsonSupport() { if (this._check('json')) { return; } - this.app.use(bodyParser.json({limit: '1mb'})); // Increase from the default 100kb + this.app.use(express.json({limit: '1mb'})); // Increase from the default 100kb } public addSessions() { diff --git a/app/server/lib/NSandbox.ts b/app/server/lib/NSandbox.ts index 05ec7c8b..ac61a033 100644 --- a/app/server/lib/NSandbox.ts +++ b/app/server/lib/NSandbox.ts @@ -737,6 +737,10 @@ function gvisor(options: ISandboxOptions): SandboxProcess { wrapperArgs.push(process.env.GRIST_CHECKPOINT!); } const child = spawn(command, [...wrapperArgs.get(), `python${pythonVersion}`, '--', ...pythonArgs]); + if (!child.pid) { + throw new Error(`failed to spawn python${pythonVersion}`); + } + // For gvisor under ptrace, main work is done by a traced process identifiable as // being labeled "exe" and having a parent also labeled "exe". const recognizeTracedProcess = (p: ProcessInfo) => { diff --git a/app/server/lib/SamlConfig.ts b/app/server/lib/SamlConfig.ts index 5cdac99a..bc51669c 100644 --- a/app/server/lib/SamlConfig.ts +++ b/app/server/lib/SamlConfig.ts @@ -52,7 +52,6 @@ * */ -import * as bodyParser from 'body-parser'; import * as express from 'express'; import * as fse from 'fs-extra'; import * as saml2 from 'saml2-js'; @@ -161,7 +160,7 @@ export class SamlConfig { })); // Assert endpoint for when the login completes as POST. - app.post("/saml/assert", bodyParser.urlencoded({extended: true}), expressWrap(async (req, res, next) => { + app.post("/saml/assert", express.urlencoded({extended: true}), expressWrap(async (req, res, next) => { const relayState: string = req.body.RelayState; if (!relayState) { throw new Error('Login or logout failed to complete'); } const permitStore = this._gristServer.getExternalPermitStore(); diff --git a/app/server/lib/SandboxControl.ts b/app/server/lib/SandboxControl.ts index d2626d1b..64341ecd 100644 --- a/app/server/lib/SandboxControl.ts +++ b/app/server/lib/SandboxControl.ts @@ -30,12 +30,16 @@ export interface ISandboxControl { * Control a single process directly. A thin wrapper around the Throttle class. */ export class DirectProcessControl implements ISandboxControl { + private _pid: number; private _throttle?: Throttle; constructor(private _process: childProcess.ChildProcess, logMeta?: log.ILogMeta) { + if (!_process.pid) { throw new Error(`process identifier (PID) is undefined`); } + + this._pid = _process.pid; if (process.env.GRIST_THROTTLE_CPU) { this._throttle = new Throttle({ - pid: _process.pid, + pid: this._pid, logMeta: {...logMeta, pid: _process.pid}, }); } @@ -55,7 +59,7 @@ export class DirectProcessControl implements ISandboxControl { } public async getUsage() { - const memory = (await pidusage(this._process.pid)).memory; + const memory = (await pidusage(this._pid)).memory; return { memory }; } } diff --git a/app/server/lib/expressWrap.ts b/app/server/lib/expressWrap.ts index 6e69bdf2..6b985a55 100644 --- a/app/server/lib/expressWrap.ts +++ b/app/server/lib/expressWrap.ts @@ -2,10 +2,16 @@ import {RequestWithLogin} from 'app/server/lib/Authorizer'; import log from 'app/server/lib/log'; import * as express from 'express'; +export type AsyncRequestHandler = ( + req: express.Request, + res: express.Response, + next: express.NextFunction +) => any | Promise; + /** * Wrapper for async express endpoints to catch errors and forward them to the error handler. */ -export function expressWrap(callback: express.RequestHandler): express.RequestHandler { +export function expressWrap(callback: AsyncRequestHandler): express.RequestHandler { return async (req, res, next) => { try { await callback(req, res, next); diff --git a/package.json b/package.json index ae4c7f75..9151269d 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "@types/diff-match-patch": "1.0.32", "@types/dompurify": "2.4.0", "@types/double-ended-queue": "2.1.0", - "@types/express": "4.16.0", + "@types/express": "4.17.17", "@types/form-data": "2.2.1", "@types/fs-extra": "5.0.4", "@types/http-proxy": "1.17.9", @@ -64,17 +64,18 @@ "@types/marked": "4.0.8", "@types/mime-types": "2.1.0", "@types/minio": "7.0.15", - "@types/mocha": "5.2.5", + "@types/mocha": "10.0.1", "@types/moment-timezone": "0.5.9", "@types/mousetrap": "1.6.2", - "@types/node": "^14", + "@types/node": "18.11.9", "@types/node-fetch": "2.6.2", "@types/pidusage": "2.0.1", "@types/plotly.js": "2.12.1", + "@types/proper-lockfile": "4.1.2", "@types/qrcode": "1.4.2", "@types/redlock": "3.0.2", "@types/saml2-js": "2.0.1", - "@types/selenium-webdriver": "4.0.0", + "@types/selenium-webdriver": "4.1.15", "@types/sinon": "5.0.5", "@types/sqlite3": "3.1.6", "@types/tmp": "0.0.33", @@ -94,10 +95,11 @@ "i18next-scanner": "4.1.0", "jsdom": "16.5.0", "mocha": "10.2.0", - "mocha-webdriver": "0.2.13", + "mocha-webdriver": "0.3.1", "moment-locales-webpack-plugin": "^1.2.0", "nodemon": "^2.0.4", "otplib": "12.0.1", + "proper-lockfile": "4.1.2", "sinon": "7.1.1", "source-map-loader": "^0.2.4", "tmp-promise": "1.0.5", @@ -137,7 +139,7 @@ "dompurify": "3.0.0", "double-ended-queue": "2.1.0-0", "exceljs": "4.2.1", - "express": "4.16.4", + "express": "4.18.2", "file-type": "16.5.4", "fs-extra": "7.0.0", "grain-rpc": "0.1.7", @@ -148,7 +150,7 @@ "https-proxy-agent": "5.0.1", "i18n-iso-countries": "6.1.0", "i18next": "21.9.1", - "i18next-http-middleware": "3.2.1", + "i18next-http-middleware": "3.3.2", "image-size": "0.6.3", "jquery": "3.5.0", "js-yaml": "3.14.1", diff --git a/static/test.html b/static/test.html index c2bf553b..af305a9d 100644 --- a/static/test.html +++ b/static/test.html @@ -24,8 +24,8 @@ } mocha.checkLeaks(); - // fxdriver_id is set by selenium, execWebdriverJQuery by webdriverjq.js. - mocha.globals(['cmd', 'fxdriver_id', 'execWebdriverJQuery']); + // fxdriver_id and ret_nodes are set by selenium, execWebdriverJQuery by webdriverjq.js. + mocha.globals(['cmd', 'fxdriver_id', 'ret_nodes', 'execWebdriverJQuery']); var runner = mocha.run(); mocha.failedTests = []; runner.on('fail', function(test, err) { diff --git a/test/declarations.d.ts b/test/declarations.d.ts index 63c06435..46b736b9 100644 --- a/test/declarations.d.ts +++ b/test/declarations.d.ts @@ -15,31 +15,3 @@ declare namespace Chai { notIncludeMembers(superset: T[], subset: T[], message?: string): void; } } - -declare module "selenium-webdriver" { - interface WebDriver { - withActions(cb: (actions: WebActions) => void): Promise; - } - - // This is not a complete definition of available methods, but only those that we use for now. - // TODO: find documentation for this interface or update selenium-webdriver. - interface WebActions { - contextClick(el?: WebElement): WebActions; - click(el?: WebElement): WebActions; - press(): WebActions; - move(params: {origin?: WebElement|string, x?: number, y?: number}): WebActions; - keyDown(key: string): WebActions; - keyUp(key: string): WebActions; - dragAndDrop(element: WebElement, target: WebElement): WebActions; - release(): WebActions; - doubleClick(element: WebElement): WebActions; - pause(ms: number): WebActions; - } -} - -import "mocha-webdriver"; -declare module "mocha-webdriver" { - // It looks like this hack makes tsc see our definition as primary, adding - // the typed version override (of the withActions method) as the default one. - export declare let driver: import("selenium-webdriver").WebDriver; -} diff --git a/test/gen-server/seed.ts b/test/gen-server/seed.ts index 5ac9dc02..7ef2d06b 100644 --- a/test/gen-server/seed.ts +++ b/test/gen-server/seed.ts @@ -23,7 +23,7 @@ */ import {addPath} from 'app-module-path'; -import {IHookCallbackContext} from 'mocha'; +import {Context} from 'mocha'; import * as path from 'path'; import {Connection, getConnectionManager, Repository} from 'typeorm'; @@ -643,7 +643,7 @@ function _generateData(numOrgs: number, numWorkspaces: number, numDocs: number) * To set up TYPEORM_* environment variables for testing, call this in a before() call of a test * suite, using setUpDB(this); */ -export function setUpDB(context?: IHookCallbackContext) { +export function setUpDB(context?: Context) { if (!process.env.TYPEORM_DATABASE) { process.env.TYPEORM_DATABASE = ":memory:"; } else { diff --git a/test/init-mocha-webdriver.js b/test/init-mocha-webdriver.js index 01745d1b..3ccfd6ee 100644 --- a/test/init-mocha-webdriver.js +++ b/test/init-mocha-webdriver.js @@ -1,6 +1,6 @@ /** * Settings that affect tests using mocha-webdriver. This module is imported by any run of mocha, - * by being listed in test/mocha.opts. (Keep in mind that it's imported by non-browser tests, such + * by being listed in package.json. (Keep in mind that it's imported by non-browser tests, such * as test/common, as well.) */ diff --git a/test/mocha.opts b/test/mocha.opts deleted file mode 100644 index cef2aeb3..00000000 --- a/test/mocha.opts +++ /dev/null @@ -1,5 +0,0 @@ ---require source-map-support/register -test/report-why-tests-hang -test/init-mocha-webdriver -test/split-tests -test/chai-as-promised diff --git a/test/nbrowser/ChartView1.ts b/test/nbrowser/ChartView1.ts new file mode 100644 index 00000000..337431e1 --- /dev/null +++ b/test/nbrowser/ChartView1.ts @@ -0,0 +1,804 @@ +import {UserAPI} from 'app/common/UserAPI'; +import {assert, driver, Key} from 'mocha-webdriver'; +import {addYAxis, checkAxisConfig, checkAxisRange, findYAxis, getAxisTitle, getChartData, + removeYAxis, selectChartType, selectXAxis, + setSplitSeries} from 'test/nbrowser/chartViewTestUtils'; +import * as gu from 'test/nbrowser/gristUtils'; +import {setupTestSuite} from 'test/nbrowser/testUtils'; + +describe('ChartView1', function() { + this.timeout(20000); + const cleanup = setupTestSuite(); + let api: UserAPI; + let doc: any; + + before(async function() { + const session = await gu.session().teamSite.login(); + doc = await session.tempDoc(cleanup, 'ChartData.grist'); + api = session.createHomeApi(); + }); + + gu.bigScreen(); + afterEach(() => gu.checkForErrors()); + + it('should allow adding and removing chart viewsections', async function() { + // Starting out with one section + assert.lengthOf(await driver.findAll('.test-gristdoc .view_leaf'), 1); + + // Add a new chart section + await gu.addNewSection(/Chart/, /ChartData/); + + // Check that there are now two sections + assert.lengthOf(await driver.findAll('.test-gristdoc .view_leaf'), 2); + + // Delete the newly added one + await gu.openSectionMenu('viewLayout', 'CHARTDATA Chart'); + await driver.find('.test-section-delete').click(); + await gu.waitForServer(); + + // Check that there is now only one section + assert.lengthOf(await driver.findAll('.test-gristdoc .view_leaf'), 1); + }); + + it('should display a bar chart by default', async function() { + // Add a new chart section, and make sure it has focus + await gu.addNewSection(/Chart/, /ChartData/); + const section = await gu.getSection('CHARTDATA Chart'); + assert.equal(await section.matches('.active_section'), true); + + const chartDom = await section.find('.test-chart-container'); + assert.equal(await chartDom.isDisplayed(), true); + + const data = (await getChartData(chartDom)).data; + assert.deepEqual(data[0].type, 'bar'); + assert.deepEqual(data[0].x, [ 6, 5, 4, 3, 2, 1 ]); + assert.deepEqual(data[0].y, [ 1, 2, 3, 4, 5, 6 ]); + }); + + it('should allow viewing raw data underlying chart', async function() { + // No raw data overlay at first + assert.isFalse(await driver.find('.test-raw-data-overlay').isPresent()); + + // Show raw data overlay + await gu.openSectionMenu('viewLayout'); + await driver.find('.test-show-raw-data').click(); + + // Test that overlay is showed. + assert.isTrue(await driver.findWait('.test-raw-data-overlay', 100).isDisplayed()); + + // Test that the widget menu doesn't have the raw data option any more + await gu.openSectionMenu('viewLayout'); + assert.isTrue(await driver.findContentWait('.grist-floating-menu li', 'Print widget', 100).isDisplayed()); + assert.isFalse(await driver.findContent('.grist-floating-menu li', 'Show raw data').isPresent()); + + // Go back and confirm that the overlay is gone again + await driver.find('.test-raw-data-close-button').click(); + assert.isFalse(await driver.find('.test-raw-data-overlay').isPresent()); + + // Open once again and close by escaping. + await gu.openSectionMenu('viewLayout'); + await driver.find('.test-show-raw-data').click(); + assert.isTrue(await driver.findWait('.test-raw-data-overlay', 100).isDisplayed()); + await gu.sendKeys(Key.ESCAPE); + assert.isFalse(await driver.find('.test-raw-data-overlay').isPresent()); + }); + + it('should update as the underlying data changes', async function() { + await gu.getCell({section: 'ChartData', col: 0, rowNum: 1}).click(); + await driver.sendKeys(Key.ENTER, '1', Key.ENTER); // Change from 6 to 61 + await gu.waitForServer(); + + const chartDom = await driver.find('.test-chart-container'); + let data = (await getChartData(chartDom)).data; + assert.deepEqual(data[0].type, 'bar'); + assert.deepEqual(data[0].x, [ 61, 5, 4, 3, 2, 1 ]); + assert.deepEqual(data[0].y, [ 1, 2, 3, 4, 5, 6 ]); + + await gu.getCell({section: 'ChartData', col: 1, rowNum: 1}).click(); + await driver.sendKeys(Key.ENTER, '6', Key.ENTER); // Change from 1 to 16 + await gu.waitForServer(); + + data = (await getChartData(chartDom)).data; + assert.deepEqual(data[0].type, 'bar'); + assert.deepEqual(data[0].x, [ 61, 5, 4, 3, 2, 1 ]); + assert.deepEqual(data[0].y, [ 16, 2, 3, 4, 5, 6 ]); + }); + + it('should skip empty points', async function() { + const chartDom = await driver.find('.test-chart-container'); + let data = (await getChartData(chartDom)).data; + assert.deepEqual(data[0].x, [ 61, 5, 4, 3, 2, 1 ]); + assert.deepEqual(data[0].y, [ 16, 2, 3, 4, 5, 6 ]); + + // Enter some blank values and a zero. The zero should be included in the plot, but blanks + // should not. + await gu.getCell({col: 1, rowNum: 1}).click(); + await driver.sendKeys(Key.DELETE); + await gu.getCell({col: 1, rowNum: 4}).click(); + await driver.sendKeys(Key.DELETE); + await gu.getCell({col: 1, rowNum: 6}).click(); + await driver.sendKeys('0', Key.ENTER); + await gu.waitForServer(); + + data = (await getChartData(chartDom)).data; + assert.deepEqual(data[0].x, [ 5, 4, 2, 1 ]); + assert.deepEqual(data[0].y, [ 2, 3, 5, 0 ]); + + // Undo and verify that the range is restored. + await gu.undo(3); + data = (await getChartData(chartDom)).data; + assert.deepEqual(data[0].x, [ 61, 5, 4, 3, 2, 1 ]); + assert.deepEqual(data[0].y, [ 16, 2, 3, 4, 5, 6 ]); + }); + + it('should update chart when new columns are included', async function() { + const chartDom = await driver.find('.test-chart-container'); + // Check to make sure intial values are correct. + let data = (await getChartData(chartDom)).data; + assert.deepEqual(data[0].type, 'bar'); + assert.deepEqual(data[0].x, [ 61, 5, 4, 3, 2, 1 ]); + assert.deepEqual(data[0].y, [ 16, 2, 3, 4, 5, 6 ]); + + // Check that the intial scales are correct for the dataset. + checkAxisRange(await getChartData(chartDom), 0.5, 61.5, 0, 16.5); + + // Open the view config pane for the Chart section. + await gu.getSection('ChartData chart').find('.viewsection_title').click(); + await gu.toggleSidePanel('right', 'open'); + await driver.find('.test-right-tab-pagewidget').click(); + await driver.find('.test-config-widget').click(); + + // Check intial visible fields. + await checkAxisConfig({ + xaxis: 'label', + yaxis: ['value'] + }); + + // Adds 'largeValue' + await driver.find('.test-chart-add-y-axis').click(); + await driver.findContent('.grist-floating-menu li', 'largeValue').click(); + await gu.waitForServer(); + + // Check axis are correct + await checkAxisConfig({ + xaxis: 'label', + yaxis: ['value', 'largeValue'] + }); + + // Move 'largeValue' above 'value'. Scroll it into view first, since dragging is a bit messed + // up when it causes the pane to scroll. + await gu.scrollIntoView(findYAxis('largeValue')); + await driver.withActions((actions) => actions.dragAndDrop(findYAxis('largeValue'), findYAxis('value'))); + await gu.waitForServer(); + + await checkAxisConfig({ + xaxis: 'label', + yaxis: ['largeValue', 'value'] + }); + + // Make sure only y axis updates to the new column of data + await driver.sleep(50); + data = (await getChartData(chartDom)).data; + assert.deepEqual(data[0].type, 'bar'); + assert.deepEqual(data[0].x, [ 61, 5, 4, 3, 2, 1 ]); + assert.deepEqual(data[0].y, [ 22, 33, 11, 44, 22, 55 ]); + assert.deepEqual(data[1].type, 'bar'); + assert.deepEqual(data[1].x, [ 61, 5, 4, 3, 2, 1 ]); + assert.deepEqual(data[1].y, [ 16, 2, 3, 4, 5, 6 ]); + + // Check that the scales are correct for the new y values. + checkAxisRange(await getChartData(chartDom), 0.5, 61.5, 0, 57); + + // select 'largeValue' as x axis + await selectXAxis('largeValue'); + + // check x-axis is correct + await checkAxisConfig({ + xaxis: 'largeValue', + yaxis: ['value'] // note: 'largeValue' was correctly removed from y-axis + }); + + // adds 'label' as y axis + await addYAxis('label'); + + // check axis are correct + await checkAxisConfig({ + xaxis: 'largeValue', + yaxis: ['value', 'label'] + }); + + // Reverse the order of the columns and make sure the data updates to reflect that. + await driver.sleep(50); + data = (await getChartData(chartDom)).data; + assert.deepEqual(data[0].type, 'bar'); + assert.deepEqual(data[0].x, [ 22, 33, 11, 44, 55 ]); + assert.deepEqual(data[0].y, [ 16, 2, 3, 4, 6 ]); + assert.deepEqual(data[1].type, 'bar'); + assert.deepEqual(data[1].x, [ 22, 33, 11, 44, 55 ]); + assert.deepEqual(data[1].y, [ 61, 5, 4, 3, 1 ]); + + // Check that the scales are correct for the new values. + checkAxisRange(await getChartData(chartDom), 5.5, 60.5, 0, 61); + + // select 'label' as x axis + await selectXAxis('label'); + + // adds 'largeValue' as y axis + await addYAxis('largeValue'); + + // moves 'largeValue' above 'value' + await driver.withActions((actions) => actions.dragAndDrop(findYAxis('largeValue'), findYAxis('value'))); + await gu.waitForServer(); + + // check axis correctness + await checkAxisConfig({ + xaxis: 'label', + yaxis: ['largeValue', 'value'] + }); + }); + + it('should be able to render different types of charts', async function() { + const chartDom = await driver.find('.test-chart-container'); + + await selectChartType('Pie Chart'); + let data = (await getChartData(chartDom)).data; + assert.deepEqual(data[0].type, 'pie'); + assert.equal(await driver.find('.test-chart-first-field-label').getText(), 'LABEL'); + await selectChartType('Line Chart'); + data = (await getChartData(chartDom)).data; + assert.deepEqual(data[0].type, 'scatter'); + // Make sure we are not grouping (which would produce names like "1 · value") + assert.equal(data[0].name, 'largeValue'); + assert.equal(data[1].name, 'value'); + assert.equal(await driver.find('.test-chart-first-field-label').getText(), 'X-AXIS'); + + await selectChartType('Area Chart'); + data = (await getChartData(chartDom)).data; + assert.deepEqual(data[0].type, 'scatter'); + assert.deepEqual(data[0].line!.shape, 'spline'); + assert.deepEqual(data[0].fill, 'tozeroy'); + assert.equal(await driver.find('.test-chart-type').getText(), 'Area Chart'); + + // Make sure first field of scatter plot is marked label, not x-axis. + await selectChartType('Scatter Plot'); + assert.equal(await driver.find('.test-chart-first-field-label').getText(), 'LABEL'); + + // Make sure first field of Kaplan-Meier plot is marked label, not x-axis. + await selectChartType('Kaplan-Meier Plot'); + assert.equal(await driver.find('.test-chart-first-field-label').getText(), 'LABEL'); + + // Return to Area Chart. + await selectChartType('Area Chart'); + }); + + it('should render pie charts with a single series, or counts', async function() { + await selectChartType('Pie Chart'); + + // select 'person' for x axis + await selectXAxis('person'); + + // adds 'label' and move to be first y axis + await addYAxis('label'); + await driver.withActions((actions) => actions.dragAndDrop(findYAxis('label'), findYAxis('largeValue'))); + await gu.waitForServer(); + + // check axis + await checkAxisConfig({ + xaxis: 'person', + yaxis: ['label', 'largeValue', 'value'] + }); + + const chartDom = await driver.find('.test-chart-container'); + let data = (await getChartData(chartDom)).data; + // Only the first series of values is included. + assert.deepEqual(data[0].values, [ 61, 4, 2, 5, 3, 1 ]); + assert.lengthOf(data, 1); + + // When no series is included, just counts are used. + await removeYAxis('largeValue'); + await removeYAxis('label'); + await removeYAxis('value'); + data = (await getChartData(chartDom)).data; + assert.deepEqual(data[0].values, [1, 1, 1, 1, 1, 1]); + assert.lengthOf(data, 1); + + await gu.undo(7); + + // check axis + await checkAxisConfig({ + xaxis: 'label', + yaxis: ['largeValue', 'value'] + }); + + // check chart type + assert.equal(await driver.find('.test-chart-type').getText(), 'Area Chart'); + }); + + it('should support Y-axis options', async function() { + const chartDom = await driver.find('.test-chart-container'); + await selectChartType('Bar Chart'); + checkAxisRange(await getChartData(chartDom), 0.5, 61.5, 0, 57); + + await driver.findContent('label', /Invert Y-axis/).find('input').click(); + await gu.waitForServer(); + checkAxisRange(await getChartData(chartDom), 0.5, 61.5, 57, 0); + + await driver.findContent('label', /Invert Y-axis/).find('input').click(); + await driver.findContent('label', /Log scale Y-axis/).find('input').click(); + await gu.waitForServer(); + checkAxisRange(await getChartData(chartDom), 0.5, 61.5, 0.22, 1.82); + + await gu.undo(4); + // check axis + await checkAxisConfig({ + xaxis: 'label', + yaxis: ['largeValue', 'value'] + }); + // check chart type + assert.equal(await driver.find('.test-chart-type').getText(), 'Area Chart'); + }); + + it('should be able to render multiseries line charts', async function() { + const chartDom = await driver.find('.test-chart-container'); + + // switch type to line chart + await selectChartType('Line Chart'); + + // pick 'largeValue' as the x axis + await selectXAxis('largeValue'); + + // set 'label' as the groupby column + await setSplitSeries('label'); + + let {data, layout} = await getChartData(chartDom); + assert.deepEqual(data[0].type, 'scatter'); + assert.deepEqual(data.map(d => d.name), ['1', '2', '3', '4', '5', '61']); + assert.equal(getAxisTitle(layout.xaxis), 'largeValue'); + assert.equal(getAxisTitle(layout.yaxis), 'value'); + + // Select person for grouping by column + await setSplitSeries('person'); + + await checkAxisConfig({ + groupingByColumn: 'person', + xaxis: 'largeValue', + yaxis: ['value'], + }); + + ({data, layout} = await getChartData(chartDom)); + assert.deepEqual(data[0].type, 'scatter'); + assert.deepEqual(data.map(d => d.name), ['Alice', 'Bob']); + assert.equal(getAxisTitle(layout.xaxis), 'largeValue'); + assert.equal(getAxisTitle(layout.yaxis), 'value'); + + // Add a second series. If we have more than one, its name should be included into the series + // names rather than in the yaxis.title. + await addYAxis('label'); + + await checkAxisConfig({ + groupingByColumn: 'person', + xaxis: 'largeValue', + yaxis: ['value', 'label'], + }); + + ({data, layout} = await getChartData(chartDom)); + assert.deepEqual(data[0].type, 'scatter'); + assert.deepEqual(data.map(d => d.name), ['Alice • value', 'Alice • label', 'Bob • value', 'Bob • label']); + assert.equal(getAxisTitle(layout.xaxis), 'largeValue'); + assert.equal(getAxisTitle(layout.yaxis), undefined); + + await gu.undo(5); + await checkAxisConfig({ + groupingByColumn: false, + xaxis: 'label', + yaxis: ['largeValue', 'value'], + }); + // check chart type + assert.equal(await driver.find('.test-chart-type').getText(), 'Area Chart'); + }); + + it('should get options for SPLIT SERIES and X AXIS in sync when table changes', async function() { + + // click change widget + await driver.findContent('button', 'Change Widget').click(); + + // click sum symbol + await driver.findContent('.test-wselect-table', 'People').click(); + + // click save + await driver.find('.test-wselect-addBtn').click(); + await gu.waitForServer(); + + // click Split series + await driver.findContent('label', 'Split series').click(); + + // open split series options + await driver.find('.test-chart-group-by-column').click(); + + // check group-data options + assert.deepEqual( + await driver.findAll('.test-select-menu li', e => e.getText()), + ['Pick a column', 'Name', 'B'] + ); + + // send ESCAPE to close menu + await driver.sendKeys(Key.ESCAPE); + + // open x axis options + await driver.find('.test-chart-x-axis').click(); + + // check x axis options + assert.deepEqual( + await driver.findAll('.test-select-menu li', e => e.getText()), + ['Name', 'B'] + ); + + // send ESCAPE to close menu + await driver.sendKeys(Key.ESCAPE); + + // undo + await gu.undo(1); + }); + + it('should get series name right when grouped column has \'\' values', async function() { + // remove series 'value' + await removeYAxis('value'); + + // add a row with person left as blank + const {retValues} = await api.applyUserActions(doc.id, [ + ['AddRecord', 'ChartData', 7, {largeValue: 44}] + ]); + await setSplitSeries('person'); + + // check that series name is correct + const data = (await getChartData()).data; + assert.deepEqual(data.map(d => d.name), ['[Blank]', 'Alice', 'Bob']); + + // remove row + await api.applyUserActions(doc.id, [ + ['RemoveRecord', 'ChartData', retValues[0]] + ]); + + // undo + await gu.undo(2); + }); + + it('should disabled split series option for pie charts', async function() { + + // start with line chart type + await selectChartType('Line Chart'); + + // check the split series option is present + assert.equal(await driver.findContent('label', /Split series/).isPresent(), true); + assert.equal(await driver.find('.test-chart-group-by-column').isPresent(), true); + + // select 'person' as the split series column + await setSplitSeries('person'); + + // check split series option + assert.equal(await driver.findContent('label', /Split series/).isPresent(), true); + assert.equal(await driver.find('.test-chart-group-by-column').isPresent(), true); + + // check axis + await checkAxisConfig({ + groupingByColumn: 'person', + xaxis: 'label', + yaxis: ['largeValue', 'value'], + }); + + // select pie chart type + await selectChartType('Pie Chart'); + + // check that the split series option is not present + assert.equal(await driver.findContent('label', /Split series/).isPresent(), false); + assert.equal(await driver.find('.test-chart-group-by-column').isPresent(), false); + + // check axis + await checkAxisConfig({ + groupingByColumn: false, + xaxis: 'label', + yaxis: ['largeValue', 'value'], + }); + assert.equal(await driver.find('.test-chart-type').getText(), 'Pie Chart'); + + // undo + await gu.undo(2); + await checkAxisConfig({ + groupingByColumn: false, + xaxis: 'label', + yaxis: ['largeValue', 'value'], + }); + assert.equal(await driver.find('.test-chart-type').getText(), 'Line Chart'); + }); + + it('should render dates properly on X-axis', async function() { + await gu.getSection('ChartData').find('.viewsection_title').click(); + + // Add a new first column. + await gu.getCell({col: 0, rowNum: 1}).click(); + // driver.sendKeys() doesn't support key combinations, but elem.sendKeys() does. + await driver.find('body').sendKeys(Key.chord(Key.ALT, Key.SHIFT, '=')); + await gu.waitForServer(); + await driver.find('.test-column-title-label').sendKeys('MyDate', Key.ENTER); + await gu.waitForServer(); + + // Convert it to Date + await gu.toggleSidePanel('right', 'open'); + await driver.find('.test-right-tab-field').click(); + await gu.setType(/Date/); + await gu.waitForServer(); + + // Enter some values. + await gu.enterGridRows({col: 0, rowNum: 1}, [ + ["2018-01-15"], ["2018-01-31"], ["2018-02-14"], ["2018-03-04"], ["2018-03-14"], ["2018-03-26"] + ]); + + // Open the view config pane for the Chart section. + await gu.getSection('ChartData chart').find('.viewsection_title').click(); + await driver.find('.test-right-tab-pagewidget').click(); + + // select MyDate for x axis + await selectXAxis('MyDate'); + + const chartDom = await driver.find('.test-chart-container'); + const {data, layout} = await getChartData(chartDom); + // This check helps understand Plotly's actual interpretation of the dates. E.g. if the range + // endpoints are like '2018-03-25 20:00', plotly is misinterpreting the timezone. + assert.deepEqual(layout.xaxis.range, ['2018-01-15', '2018-03-26']); + assert.deepEqual(data[0].type, 'scatter'); + assert.deepEqual(data[0].name, 'largeValue'); + assert.deepEqual(data[0].x, [ + "2018-01-15T00:00:00.000Z", "2018-01-31T00:00:00.000Z", "2018-02-14T00:00:00.000Z", + "2018-03-04T00:00:00.000Z", "2018-03-14T00:00:00.000Z", "2018-03-26T00:00:00.000Z" + ]); + assert.deepEqual(data[0].y, [22, 33, 11, 44, 22, 55]); + assert.deepEqual(data[1].type, 'scatter'); + assert.deepEqual(data[1].name, 'value'); + assert.deepEqual(data[0].x, [ + "2018-01-15T00:00:00.000Z", "2018-01-31T00:00:00.000Z", "2018-02-14T00:00:00.000Z", + "2018-03-04T00:00:00.000Z", "2018-03-14T00:00:00.000Z", "2018-03-26T00:00:00.000Z" + ]); + assert.deepEqual(data[1].y, [16, 2, 3, 4, 5, 6]); + }); + + it('should support error bars', async function() { + // We start with a line chart with MyDate on X-axis, and two series: largeValue and value. + await selectChartType('Line Chart'); + await checkAxisConfig({xaxis: 'MyDate', yaxis: ['largeValue', 'value']}); + + // Symmetric error bars should leave only the largeValue series, with 'value' for error bars. + await driver.find('.test-chart-error-bars .test-select-open').click(); + await driver.findContent('.test-select-menu li', /Symmetric/).click(); + await gu.waitForServer(); + + const chartDom = await driver.find('.test-chart-container'); + let data = (await getChartData(chartDom)).data; + assert.deepEqual(data[0].type, 'scatter'); + assert.deepEqual(data[0].name, 'largeValue'); + assert.deepEqual(data[0].y, [22, 33, 11, 44, 22, 55]); + assert.deepEqual((data[0].error_y as any).array, [16, 2, 3, 4, 5, 6]); + assert.deepEqual(data[0].error_y!.symmetric, true); + assert.lengthOf(data, 1); + + // Using separate error bars for above+below will leave just the "above" error bars. + await driver.find('.test-chart-error-bars .test-select-open').click(); + await driver.findContent('.test-select-menu li', /Above.*Below/).click(); + await gu.waitForServer(); + data = (await getChartData(chartDom)).data; + assert.deepEqual(data[0].y, [22, 33, 11, 44, 22, 55]); + assert.deepEqual((data[0].error_y as any).array, [16, 2, 3, 4, 5, 6]); + assert.deepEqual((data[0].error_y as any).arrayminus, null); + assert.deepEqual(data[0].error_y!.symmetric, false); + assert.lengthOf(data, 1); + + // If we add another line, it'll be used for "below" error bars. + await addYAxis('label'); + data = (await getChartData(chartDom)).data; + assert.deepEqual(data[0].y, [22, 33, 11, 44, 22, 55]); + assert.deepEqual((data[0].error_y as any).array, [16, 2, 3, 4, 5, 6]); + assert.deepEqual((data[0].error_y as any).arrayminus, [61, 5, 4, 3, 2, 1]); + assert.deepEqual(data[0].error_y!.symmetric, false); + assert.lengthOf(data, 1); + + // Should work also for bar charts + await selectChartType('Bar Chart'); + data = (await getChartData(chartDom)).data; + assert.deepEqual(data[0].type, 'bar'); + assert.deepEqual(data[0].y, [22, 33, 11, 44, 22, 55]); + assert.deepEqual((data[0].error_y as any).array, [16, 2, 3, 4, 5, 6]); + assert.deepEqual((data[0].error_y as any).arrayminus, [61, 5, 4, 3, 2, 1]); + assert.deepEqual(data[0].error_y!.symmetric, false); + assert.lengthOf(data, 1); + await gu.undo(1); + + + await gu.undo(3); + }); + + it('should fetch data for tables not yet loaded', async function() { + // Create a Page that only has a Chart, no other sections. + await gu.addNewPage(/Chart/, /ChartData/); + + let chartDom = await driver.findWait('.test-chart-container', 1000); + assert.equal(await chartDom.isDisplayed(), true); + let data = (await getChartData(chartDom)).data; + assert.lengthOf(data, 1); + assert.deepEqual(data[0].type, 'bar'); + assert.deepEqual(data[0].y, [ 61, 5, 4, 3, 2, 1 ]); + + // Reload the page and test that the chart loaded. + await driver.navigate().refresh(); + await gu.waitForDocToLoad(); + + await driver.sleep(1000); + chartDom = await driver.findWait('.test-chart-container', 1000); + assert.equal(await chartDom.isDisplayed(), true); + data = (await getChartData(chartDom)).data; + assert.lengthOf(data, 1); + assert.deepEqual(data[0].type, 'bar'); + assert.deepEqual(data[0].y, [ 61, 5, 4, 3, 2, 1 ]); + }); + + it('should resize chart when side panels open or close', async function() { + // Open a document with some chart data. + const session = await gu.session().teamSite.login(); + doc = await session.tempDoc(cleanup, 'ChartData.grist'); + await gu.toggleSidePanel('right', 'close'); + + // Add a chart section. + await gu.addNewSection(/Chart/, /ChartData/); + const chart = await driver.findWait('.viewsection_content .svg-container', 1000); + const initialRect = await chart.getRect(); + // We expect the left panel open initially. + assert.equal(await gu.isSidePanelOpen('left'), true); + + // Open the RightPanel, check that chart's width was reduced. + await gu.toggleSidePanel('right', 'open'); + await driver.wait(async () => (await chart.getRect()).width < initialRect.width, 1000); + + // Close the panel and check the chart went back to initial size. + await gu.toggleSidePanel('right', 'close'); + await driver.wait(async () => (await chart.getRect()).width === initialRect.width, 1000); + assert.deepEqual(await chart.getRect(), initialRect); + + // Close the left panel, and check that chart width was increased. + await gu.toggleSidePanel('left', 'close'); + await driver.wait(async () => (await chart.getRect()).width > initialRect.width, 1000); + + // Reopen the left panel and check the chart went back to initial size. + await gu.toggleSidePanel('left', 'open'); + await driver.wait(async () => (await chart.getRect()).width === initialRect.width, 1000); + assert.deepEqual(await chart.getRect(), initialRect); + }); + + // Tests a bug where js errors would be thrown when fewer than 2 series were visible + // and any chart settings were changed. + it('should not throw errors when no y-axis are set', async function() { + // Open the RightPanel and hide both series. + await gu.toggleSidePanel('right', 'open'); + await removeYAxis('value'); + + // Invert the y-axis. (This is meant to trigger js errors if the bug is present) + await driver.findContent('label', /Invert Y-axis/).find('input').click(); + await gu.waitForServer(); + + // Group by the first column. (This is meant to trigger js errors if the bug is present) + await setSplitSeries('value'); + + // Disable groupby column + await setSplitSeries(false); + + // Revert changes. + await gu.undo(3); + }); + + // Tests a bug where hitting enter would try to edit a non-existent cell for summary charts. + it('should not throw errors when pressing enter on summary charts', async function() { + // Click the section and press 'Enter'. + await gu.getSection('ChartData chart').click(); + await driver.sendKeys(Key.ENTER); + await gu.checkForErrors(); + }); + + it('should not throw errors when switching to a chart page', async function() { + await gu.getPageItem('People').click(); + await gu.waitForServer(); + await gu.getPageItem('ChartData').click(); + await gu.waitForServer(); + const chartDom = await gu.getSection('ChartData chart').find('.test-chart-container'); + assert.equal(await chartDom.isDisplayed(), true); + await gu.checkForErrors(); + }); + + it('should not throw errors when summarizing or un-summarizing underlying table', async function() { + // activate the chart widget + await gu.getSection('ChartData chart').click(); + + // open widget option + await gu.openSectionMenu('viewLayout'); + await driver.findContent('.grist-floating-menu li', 'Widget options').click(); + + // open the page widget picker + await driver.findContent('.test-right-panel button', 'Change Widget').click(); + + // click the summarize button + await driver.findContent('.test-wselect-table', 'ChartData').find('.test-wselect-pivot').click(); + + // click save + await driver.find('.test-wselect-addBtn').click(); + + // wait for server + await gu.waitForServer(); + + // wait for chart to be changed + await gu.waitToPass(async () => { + assert.equal( + await gu.getActiveSectionTitle(), + 'CHARTDATA [Totals] Chart' + ); + }); + + // check for error + await gu.checkForErrors(); + + // undo 1 + await gu.undo(1); + }); + + it('should sort x-axis values', async function() { + // Import a small table of numbers to test this. + await gu.importFileDialog('uploads/ChartData-Sort_Test.csv'); + + await driver.find('.test-modal-confirm').click(); + await gu.waitForServer(); + + // Add a chart of this data, and configure it first to just show X and Y1, Y2 series. + await gu.addNewSection(/Chart/, /ChartData-Sort_Test/); + await gu.toggleSidePanel('right', 'open'); + await selectChartType('Line Chart'); + + // Show series X, Y1, Y2, grouped by Group. + await selectXAxis('X'); + await setSplitSeries('Group'); + await addYAxis('Y1'); + await addYAxis('Y2'); + + const chartDom = await driver.findWait('.test-chart-container', 1000); + let {data} = await getChartData(chartDom); + assert.lengthOf(data, 4); + assert.deepInclude(data[0], {type: 'scatter', name: 'Bar • Y1'}); + assert.deepInclude(data[1], {type: 'scatter', name: 'Bar • Y2'}); + assert.deepInclude(data[2], {type: 'scatter', name: 'Foo • Y1'}); + assert.deepInclude(data[3], {type: 'scatter', name: 'Foo • Y2'}); + assert.deepEqual(data[0].x, [ 1.5, 2.5, 3.5, 4.5, 5.5 ]); + assert.deepEqual(data[0].y, [ 1.5, 1, 3.5, 2.5, 4 ]); + assert.deepEqual(data[1].x, [ 1.5, 2.5, 3.5, 4.5, 5.5 ]); + assert.deepEqual(data[1].y, [ 6.9, 6, 4.9, 5, 7 ]); + assert.deepEqual(data[2].x, [ 1, 2, 3, 4, 5 ]); + assert.deepEqual(data[2].y, [ 1.5, 1, 3.5, 2.5, 4 ]); + assert.deepEqual(data[3].x, [ 1, 2, 3, 4, 5 ]); + assert.deepEqual(data[3].y, [ 6.9, 6, 4.9, 5, 7 ]); + + // Now show series ungrouped. + await setSplitSeries(false); + + ({data} = await getChartData(chartDom)); + assert.lengthOf(data, 2); + assert.deepInclude(data[0], {type: 'scatter', name: 'Y1'}); + assert.deepInclude(data[1], {type: 'scatter', name: 'Y2'}); + assert.deepEqual(data[0].x, [ 1, 1.5, 2, 2.5, 3, 3.5, 4, 4.5, 5, 5.5 ]); + assert.deepEqual(data[0].y, [ 1.5, 1.5, 1, 1, 3.5, 3.5, 2.5, 2.5, 4, 4 ]); + assert.deepEqual(data[1].x, [ 1, 1.5, 2, 2.5, 3, 3.5, 4, 4.5, 5, 5.5 ]); + assert.deepEqual(data[1].y, [ 6.9, 6.9, 6, 6, 4.9, 4.9, 5, 5, 7, 7 ]); + }); + + it('should not throw when picking the grouping by column for the x-axis', async function() { + await checkAxisConfig({xaxis: 'X', yaxis: ['Y1', 'Y2']}); + await setSplitSeries('Group'); + await checkAxisConfig({xaxis: 'X', yaxis: ['Y1', 'Y2'], groupingByColumn: 'Group'}); + await selectXAxis('Group'); + await checkAxisConfig({xaxis: 'Group', yaxis: ['Y1', 'Y2']}); + await gu.checkForErrors(); + await gu.undo(2); + }); +}); diff --git a/test/nbrowser/ChoiceList.ts b/test/nbrowser/ChoiceList.ts index 2267e039..e572646d 100644 --- a/test/nbrowser/ChoiceList.ts +++ b/test/nbrowser/ChoiceList.ts @@ -137,6 +137,7 @@ async function saveChoiceEntries() { describe('ChoiceList', function() { this.timeout(20000); const cleanup = setupTestSuite(); + const clipboard = gu.getLockableClipboard(); const WHITE_FILL = 'rgba(255, 255, 255, 1)'; const UNSET_FILL = WHITE_FILL; @@ -748,12 +749,17 @@ describe('ChoiceList', function() { await gu.sendKeys('Choice 1', Key.ENTER, 'Choice 2', Key.ENTER, 'Choice 3', Key.ENTER); // Copy all the choices - await gu.sendKeys(Key.chord(modKey, 'a'), await gu.copyKey()); + await gu.sendKeys(await gu.selectAllKey()); + await clipboard.lockAndPerform(async (cb) => { + await cb.copy(); - // Delete all the choices, then paste them back and verify no data was lost. - await driver.sendKeys(Key.BACK_SPACE); - assert.deepEqual(await getEditModeChoiceLabels(), []); - await gu.sendKeys(await gu.pasteKey()); + // Delete all the choices, then paste them back + await driver.sendKeys(Key.BACK_SPACE); + assert.deepEqual(await getEditModeChoiceLabels(), []); + await cb.paste(); + }); + + // Verify no data was lost assert.deepEqual(await getEditModeChoiceLabels(), ['Choice 1', 'Choice 2', 'Choice 3']); // In Jenkins, clipboard contents are pasted from the system clipboard, which only copies @@ -846,14 +852,18 @@ describe('ChoiceList', function() { // Make sure we can copy and paste without adding new item await clickEntry('foo'); - await gu.sendKeys(await gu.cutKey()); - await gu.sendKeys(await gu.pasteKey()); - await gu.sendKeys(await gu.pasteKey()); + await clipboard.lockAndPerform(async (cb) => { + await cb.cut(); + await cb.paste(); + await cb.paste(); + }); await gu.sendKeys(Key.ENTER); await clickEntry('two'); - await gu.sendKeys(await gu.copyKey()); - await gu.sendKeys(Key.ARROW_RIGHT); - await gu.sendKeys(await gu.pasteKey()); + await clipboard.lockAndPerform(async (cb) => { + await cb.copy(); + await gu.sendKeys(Key.ARROW_RIGHT); + await cb.paste(); + }); await gu.sendKeys(Key.ENTER); assert.deepEqual(await getEditModeChoiceLabels(), ["foofoo", "three", "twotwo"]); await saveChoiceEntries(); diff --git a/test/nbrowser/CopyPaste.ts b/test/nbrowser/CopyPaste.ts new file mode 100644 index 00000000..a7e8fc9f --- /dev/null +++ b/test/nbrowser/CopyPaste.ts @@ -0,0 +1,676 @@ +/** + * Test for copy-pasting Grist data. + * + * TODO Most of the testing for copy-pasting lives in test/nbrowser/CopyPaste.ntest.js. + * This file just has some more recent additions to these test. + */ +import {arrayRepeat} from 'app/common/gutil'; +import * as _ from 'lodash'; +import {assert, driver, Key, WebElement} from 'mocha-webdriver'; +import * as path from 'path'; +import {serveStatic} from 'test/nbrowser/customUtil'; +import * as gu from 'test/nbrowser/gristUtils'; +import {setupTestSuite} from 'test/nbrowser/testUtils'; + +describe('CopyPaste', function() { + this.timeout(60000); + const cleanup = setupTestSuite(); + const clipboard = gu.getLockableClipboard(); + afterEach(() => gu.checkForErrors()); + gu.bigScreen(); + + after(async function() { + await driver.executeScript(removeDummyTextArea); + }); + + it('should allow pasting merged cells', async function() { + // Test that we can paste uneven data, i.e. containing merged cells. + + // Serve a static file with a page containing a table with some merged cells. + const serving = await serveStatic(path.join(gu.fixturesRoot, "sites/paste")); + await driver.get(`${serving.url}/paste.html`); + + // Select everything in our little page. + await driver.executeScript(` + let range = document.createRange(); + range.selectNodeContents(document.querySelector('table')); + let sel = window.getSelection(); + sel.removeAllRanges(); + sel.addRange(range); + `); + + await clipboard.lockAndPerform(async (cb) => { + try { + await cb.copy(); + } finally { + await serving?.shutdown(); + } + + const session = await gu.session().login(); + await session.tempNewDoc(cleanup, 'CopyPaste'); + + await gu.getCell({col: 'A', rowNum: 1}).click(); + await gu.waitAppFocus(); + await cb.paste(); + }); + await gu.waitForServer(); + + await gu.checkForErrors(); + assert.deepEqual(await gu.getVisibleGridCells({rowNums: [1, 2, 3, 4], cols: ['A', 'B']}), [ + 'a', 'b', + 'c', '', + 'd', 'e', + 'f', '', + ]); + }); + + it('should parse pasted numbers', async function() { + const session = await gu.session().teamSite.login(); + await session.tempDoc(cleanup, 'PasteParsing.grist'); + await driver.executeScript(createDummyTextArea); + + await clipboard.lockAndPerform(async (cb) => { + await copyAndCheck(cb, [ + '$1', '1', + '(2)', '-2', + '3e4', '30000', + '5,678.901', '5678.901', + '23%', '0.23', + '45 678', '45678', + + // . is a decimal separator in this locale (USA) so this can't be parsed + '1.234.567', '1.234.567 INVALID', + + // Doesn't match the default currency of the document, whereas $ above does + '€89', '€89 INVALID', + ], true); + }); + + // Open the side panel for the numeric column. + await gu.toggleSidePanel('right', 'open'); + await driver.find('.test-right-tab-field').click(); + + // Switch to currency mode, and check the result. + await driver.findContent('.test-numeric-mode .test-select-button', /\$/).click(); + + // Same data, just formatted differently + await checkGridCells([ + '$1', '$1.00', + '(2)', '-$2.00', + '3e4', '$30,000.00', + '5,678.901', '$5,678.90', + '23%', '$0.23', + '45 678', '$45,678.00', + '1.234.567', '1.234.567 INVALID', + '€89', '€89 INVALID', + ]); + + // Check that currency is set to 'Default currency' by default (where the default is local currency). + assert.equal(await driver.find('.test-currency-autocomplete input').value(), 'Default currency (USD)'); + + // Change column setting for currency to Euros + await driver.findWait('.test-currency-autocomplete', 500).click(); + await driver.sendKeys("eur", Key.ENTER); + await gu.waitForServer(); + + // Same data, just formatted differently + await checkGridCells([ + '$1', '€1.00', + '(2)', '-€2.00', + '3e4', '€30,000.00', + '5,678.901', '€5,678.90', + '23%', '€0.23', + '45 678', '€45,678.00', + '1.234.567', '1.234.567 INVALID', + '€89', '€89 INVALID', + ]); + + // Copy the numbers column into itself. + // Values which were already parsed remain parsed since it copies the underlying numbers. + await clipboard.lockAndPerform(async (cb) => { + await copy(cb, 'Parsed'); + }); + await checkGridCells([ + '$1', '€1.00', + '(2)', '-€2.00', + '3e4', '€30,000.00', + '5,678.901', '€5,678.90', + '23%', '€0.23', + '45 678', '€45,678.00', + '1.234.567', '1.234.567 INVALID', + + // This was invalid before, so it was copied as text. + // This time it parsed successfully because the currency matches. + '€89', '€89.00', + ]); + + await clipboard.lockAndPerform(async (cb) => { + await copyAndCheck(cb, [ + // Now we're copying from the text column so everything is parsed again. + // $ can no longer be parsed now the currency is euros. + '$1', '$1 INVALID', + + '(2)', '-€2.00', + '3e4', '€30,000.00', + '5,678.901', '€5,678.90', + '23%', '€0.23', + '45 678', '€45,678.00', + '1.234.567', '1.234.567 INVALID', + '€89', '€89.00', + ], true); + }); + + // Change the document locale + await gu.openDocumentSettings(); + await driver.findWait('.test-locale-autocomplete', 500).click(); + await driver.sendKeys("Germany", Key.ENTER); + await gu.waitForServer(); + await driver.navigate().back(); + + // Same data, just formatted differently + // Currency sign has moved to the end + // Decimal separator is now ',' + // Digit group separator is now '.' + await checkGridCells([ + '$1', '$1 INVALID', + '(2)', '-2,00 €', + '3e4', '30.000,00 €', + '5,678.901', '5.678,90 €', + '23%', '0,23 €', + '45 678', '45.678,00 €', + '1.234.567', '1.234.567 INVALID', + '€89', '89,00 €', + ]); + + // Copy the numbers column into itself. + // Values which were already parsed don't change since it copies the underlying numbers. + await clipboard.lockAndPerform(async (cb) => { + await copy(cb, 'Parsed'); + }); + await checkGridCells([ + '$1', '$1 INVALID', + '(2)', '-2,00 €', + '3e4', '30.000,00 €', + '5,678.901', '5.678,90 €', + '23%', '0,23 €', + '45 678', '45.678,00 €', + + // This can be parsed for the first time now that '.' + // is seen as a digit group separator + '1.234.567', '1.234.567,00 €', + + '€89', '89,00 €', + ]); + + await clipboard.lockAndPerform(async (cb) => { + await copyAndCheck(cb, [ + '$1', '$1 INVALID', + '(2)', '-2,00 €', + '3e4', '30.000,00 €', + + // Now we're copying from the text column so everything is parsed again. + // The result in this case is not good: + // '.' was simply removed because we don't check where it is + // ',' is the decimal separator + // So this is parsed as 5.678901 + // which rounds to 5.68 to two decimal places for the currency format + '5,678.901', '5,68 €', + + '23%', '0,23 €', + '45 678', '45.678,00 €', + '1.234.567', '1.234.567,00 €', + '€89', '89,00 €', + ], true); + }); + }); + + it('should parse pasted dates', async function() { + await gu.getPageItem("Dates").click(); + + await clipboard.lockAndPerform(async (cb) => { + await copyAndCheck(cb, [ + '01-02-03', '01-02-2003', + '01 02 2003', '01-02-2003', + '1/02/03', '01-02-2003', + '01/2/03', '01-02-2003', + '1/2/03', '01-02-2003', + '1/2/3', '1/2/3 INVALID', + '20/10/03', '20-10-2003', + '10/20/03', '10/20/03 INVALID', + ]); + }); + + await gu.getCell({col: 'Parsed', rowNum: 1}).click(); + assert.equal(await gu.getDateFormat(), "DD-MM-YYYY"); + await gu.setDateFormat("MM-DD-YYYY"); + + // Same data, just formatted differently + await checkGridCells([ + '01-02-03', '02-01-2003', + '01 02 2003', '02-01-2003', + '1/02/03', '02-01-2003', + '01/2/03', '02-01-2003', + '1/2/03', '02-01-2003', + '1/2/3', '1/2/3 INVALID', + '20/10/03', '10-20-2003', + '10/20/03', '10/20/03 INVALID', + ]); + + // Copy the parsed column into itself. + // Values which were already parsed don't change since it copies the underlying values. + await clipboard.lockAndPerform(async (cb) => { + await copy(cb, 'Parsed'); + }); + + await checkGridCells([ + '01-02-03', '02-01-2003', + '01 02 2003', '02-01-2003', + '1/02/03', '02-01-2003', + '01/2/03', '02-01-2003', + '1/2/03', '02-01-2003', + '1/2/3', '1/2/3 INVALID', + '20/10/03', '10-20-2003', + '10/20/03', '10-20-2003', // can be parsed now + ]); + + // Copy from the text column again, things get re-parsed + await clipboard.lockAndPerform(async (cb) => { + await copyAndCheck(cb, [ + '01-02-03', '01-02-2003', + '01 02 2003', '01-02-2003', + '1/02/03', '01-02-2003', + '01/2/03', '01-02-2003', + '1/2/03', '01-02-2003', + '1/2/3', '1/2/3 INVALID', + '20/10/03', '20/10/03 INVALID', // newly invalid + '10/20/03', '10-20-2003', + ]); + }); + }); + + // Note that these tests which reference other tables + // assume that the previous tests have run. + it('should parse pasted references', async function() { + await gu.getPageItem("References").click(); + await gu.getCell({col: 'Parsed', rowNum: 1}).click(); + assert.equal(await gu.getRefTable(), "Dates"); + assert.equal(await gu.getRefShowColumn(), "Text"); + + // Initially the References.Parsed column is displaying Dates.Text + // No date parsing happens, we just see which strings exist in that column + await clipboard.lockAndPerform(async (cb) => { + await copyAndCheck(cb, [ + '20/10/03', '20/10/03', + '10/20/03', '10/20/03', + '1/2/3', '1/2/3', + 'foo', 'foo INVALID', + '3', '3 INVALID', + '-2', '-2 INVALID', + '$1', '$1 INVALID', + '€89', '€89 INVALID', + ], true); + }); + + await gu.setRefShowColumn("Parsed"); + + // // Same data, just formatted differently + await checkGridCells([ + // In the Parsed column, only the second value was parsed as an actual date + // The others look invalid in the Dates table, but here they're valid references + '20/10/03', '20/10/03', + '10/20/03', '10-20-2003', + '1/2/3', '1/2/3', + + 'foo', 'foo INVALID', + '3', '3 INVALID', + '-2', '-2 INVALID', + '$1', '$1 INVALID', + '€89', '€89 INVALID', + ]); + + await clipboard.lockAndPerform(async (cb) => { + await copyAndCheck(cb, [ + '20/10/03', '20/10/03', + '10/20/03', '10-20-2003', + '1/2/3', '1/2/3', + 'foo', 'foo INVALID', + '3', `3 INVALID`, + '-2', `-2 INVALID`, + '$1', `$1 INVALID`, + '€89', '€89 INVALID', + ]); + }); + + await gu.setRefShowColumn("Row ID"); + + // Same data, just formatted differently + await checkGridCells([ + '20/10/03', 'Dates[5]', + '10/20/03', 'Dates[6]', + '1/2/3', 'Dates[4]', + 'foo', 'foo INVALID', + '3', `3 INVALID`, + '-2', `-2 INVALID`, + '$1', `$1 INVALID`, + '€89', '€89 INVALID', + ]); + + await clipboard.lockAndPerform(async (cb) => { + await copyAndCheck(cb, [ + '20/10/03', '20/10/03 INVALID', + '10/20/03', '10/20/03 INVALID', + '1/2/3', '1/2/3 INVALID', + 'foo', 'foo INVALID', + '3', 'Dates[3]', // 3 is the only valid Row ID + '-2', '-2 INVALID', + '$1', '$1 INVALID', + '€89', '€89 INVALID', + ]); + }); + + await gu.setRefTable("Numbers"); + + // These checks run with References.Parsed as both a Reference and Reference List column. + async function checkRefsToNumbers() { + await gu.setRefShowColumn("Row ID"); + + await clipboard.lockAndPerform(async (cb) => { + await copyAndCheck(cb, [ + '20/10/03', '20/10/03 INVALID', + '10/20/03', '10/20/03 INVALID', + '1/2/3', '1/2/3 INVALID', + 'foo', 'foo INVALID', + '3', 'Numbers[3]', + '-2', '-2 INVALID', + '$1', '$1 INVALID', + '€89', '€89 INVALID', + ], true); + }); + + await gu.setRefShowColumn("Text"); + + await clipboard.lockAndPerform(async (cb) => { + await copyAndCheck(cb, [ + '20/10/03', '20/10/03 INVALID', + '10/20/03', '10/20/03 INVALID', + '1/2/3', '1/2/3 INVALID', + 'foo', 'foo INVALID', + '3', '3 INVALID', + '-2', '-2 INVALID', + // These are the only strings that appear in Numbers.Text verbatim + '$1', '$1', + '€89', '€89', + ]); + }); + + await gu.setRefShowColumn("Parsed"); + + // Same data, just formatted differently + await checkGridCells([ + '20/10/03', '20/10/03 INVALID', + '10/20/03', '10/20/03 INVALID', + '1/2/3', '1/2/3 INVALID', + 'foo', 'foo INVALID', + '3', '3 INVALID', + '-2', '-2 INVALID', + '$1', '$1', + '€89', '89,00 €', + ]); + + await clipboard.lockAndPerform(async (cb) => { + await copyAndCheck(cb, [ + '20/10/03', '20/10/03 INVALID', + '10/20/03', '10/20/03 INVALID', + '1/2/3', '1/2/3 INVALID', + 'foo', 'foo INVALID', + '3', '3 INVALID', // parsed, but not a valid reference + '-2', '-2,00 €', + '$1', '$1', // invalid in Numbers.parsed, but a valid reference + '€89', '89,00 €', + ]); + }); + } + + await checkRefsToNumbers(); + + // Copy the Parsed column into the same column in a forked document. + // Because it's a different document, it uses the display values instead of the raw values (row IDs) + // to avoid referencing the wrong rows. + await clipboard.lockAndPerform(async (cb) => { + await copy(cb, 'Parsed'); + await driver.get(await driver.getCurrentUrl() + "/m/fork"); + await gu.waitForDocToLoad(); + await driver.executeScript(createDummyTextArea); + await gu.setRefShowColumn("Text"); + await paste(cb); + }); + await checkGridCells([ + '20/10/03', '20/10/03 INVALID', + '10/20/03', '10/20/03 INVALID', + '1/2/3', '1/2/3 INVALID', + 'foo', 'foo INVALID', + '3', '3 INVALID', + '-2', '-2,00 € INVALID', + '$1', '$1', + '€89', '89,00 € INVALID', + ]); + + // Test the main copies with the Numbers table data not loaded in the browser + // so the lookups get done in the data engine. + await checkRefsToNumbers(); + + // Now test that pasting the same values into a Reference List column + // produces the same result (reflists containing a single reference) + await gu.setType(/Reference List/); + await gu.applyTypeTransform(); + await gu.waitForServer(); + + // Clear the Parsed column. Make sure we don't edit the column header. + await gu.getCell({col: "Parsed", rowNum: 1}).click(); + await gu.getColumnHeader({col: "Parsed"}).click(); + await gu.sendKeys(Key.BACK_SPACE); + await gu.waitForServer(); + + await checkRefsToNumbers(); + }); + + it('should parse pasted reference lists containing multiple values', async function() { + async function checkMultiRefs() { + await gu.setRefShowColumn("Row ID"); + + await clipboard.lockAndPerform(async (cb) => { + await copyAndCheck(cb, [ + '"(2)",$1', '"(2)",$1 INVALID', + '$1,(2),22', '$1,(2),22 INVALID', + '["$1",-2]', '["$1",-2] INVALID', + '1,-2', '1,-2 INVALID', + '3,5', 'Numbers[3]\nNumbers[5]', // only valid row IDs + '-2,30000', '-2,30000 INVALID', + '7,0', '7,0 INVALID', // 0 is not a valid row ID + '', '', + ]); + }); + + await gu.setRefShowColumn("Text"); + + await clipboard.lockAndPerform(async (cb) => { + await copyAndCheck(cb, [ + '"(2)",$1', '(2)\n$1', // only verbatim text + '$1,(2),22', '$1,(2),22 INVALID', // 22 is invalid so whole thing fails + '["$1",-2]', '["$1",-2] INVALID', // -2 is invalid because this is text, not parsed + '1,-2', '1,-2 INVALID', + '3,5', '3,5 INVALID', + '-2,30000', '-2,30000 INVALID', + '7,0', '7,0 INVALID', + '', '', + ]); + }); + + await gu.setRefShowColumn("Parsed"); + + await clipboard.lockAndPerform(async (cb) => { + await copyAndCheck(cb, [ + '"(2)",$1', '-2,00 €\n$1', + '$1,(2),22', '$1,(2),22 INVALID', + '["$1",-2]', '$1\n-2,00 €', + '1,-2', '1,-2 INVALID', + '3,5', '3,5 INVALID', + '-2,30000', '-2,00 €\n30.000,00 €', + '7,0', '7,0 INVALID', + '', '', + ], true); + }); + } + + await gu.getPageItem("Multi-References").click(); + await gu.waitForServer(); + await gu.getCell({col: 'Parsed', rowNum: 1}).click(); + + await checkMultiRefs(); + + // Load the Numbers table data in the browser and check again + await gu.getPageItem("Numbers").click(); + await gu.getPageItem("Multi-References").click(); + await gu.waitForServer(); + await checkMultiRefs(); + }); + + it('should parse pasted choice lists', async function() { + await gu.getPageItem("ChoiceLists").click(); + await gu.waitForServer(); + + await clipboard.lockAndPerform(async (cb) => { + await copyAndCheck(cb, [ + '', '', + 'a', 'a', + + // On the left, \n in text affects parsing and separates choices + // On the right, \n is how choices are separated in .getText() + // So the newlines on the two sides match, but also "e,f" -> "e\nf" + 'a b\nc d\ne,f', 'a b\nc d\ne\nf', + + // CSVs + 'a,b ', 'a\nb', + ' "a ", b,"a,b " ', 'a\nb\na,b', + + // JSON. Empty strings and null are removed + ' ["a","b","a,b", null] ', 'a\nb\na,b', + + // Nested JSON is formatted as JSON or CSV depending on nesting level + '["a","b",["a,b"], [["a,b"]], [["a", "b"], "c", "d"], "", " "]', + 'a\nb\n"a,b"\n[["a,b"]]\n[["a", "b"], "c", "d"]', + + '[]', '', + ], true); + }); + }); + + it('should parse pasted datetimes', async function() { + await gu.getPageItem("DateTimes").click(); + await gu.waitForServer(); + + await clipboard.lockAndPerform(async (cb) => { + await copyAndCheck(cb, [ + '2021-11-12 22:57:17+03:00', '12-11-2021 21:57 SAST', // note the 1-hour difference + '2021-11-12 22:57:17+02:00', '12-11-2021 22:57 SAST', + '12-11-2021 22:57:17 SAST', '12-11-2021 22:57 SAST', + '12-11-2021 22:57:17', '12-11-2021 22:57 SAST', + '12-11-2021 22:57:17 UTC', '13-11-2021 00:57 SAST', // note the 2-hour difference + '12-11-2021 22:57:17 Z', '13-11-2021 00:57 SAST', // note the 2-hour difference + // EST doesn't match the current timezone so it's rejected + '12-11-2021 22:57:17 EST', '12-11-2021 22:57:17 EST INVALID', + // Date without time is allowed + '12-11-2021', '12-11-2021 00:00 SAST', + ]); + }); + }); +}); + + +// mapper for getVisibleGridCells to get both text and whether the cell is invalid (pink). +// Invalid cells mean text that was not parsed to the column type. +async function mapper(el: WebElement) { + let text = await el.getText(); + if (await el.find(".field_clip").matches(".invalid")) { + text += " INVALID"; + } + return text; +} + +// Checks that the full grid is equal to the given argument +// The first column never changes, it's only included for readability of the test +async function checkGridCells(expected: string[]) { + const actual = await gu.getVisibleGridCells({rowNums: _.range(1, 9), cols: ['Text', 'Parsed'], mapper}); + assert.deepEqual(actual, expected); +} + +// Paste whatever's in the clipboard into the Parsed column +async function paste(cb: gu.IClipboard) { + // Click the first cell rather than the column header so that it doesn't try renaming the column + await gu.getCell({col: 'Parsed', rowNum: 1}).click(); + await cb.paste(); + await gu.waitForServer(); + await gu.checkForErrors(); +} + +// Copy the contents of fromCol into the Parsed column +async function copy(cb: gu.IClipboard, fromCol: 'Text' | 'Parsed') { + await gu.getColumnHeader({col: fromCol}).click(); + await cb.copy(); + await paste(cb); +} + +async function copyAndCheck(cb: gu.IClipboard, expected: string[], extraChecks: boolean = false) { + // Copy Text cells into the Parsed column + await copy(cb, 'Text'); + await checkGridCells(expected); + + // Tests some extra features of parsing that don't really depend on the column + // type and so don't need to be checked with every call to copyAndCheck + if (extraChecks) { + // With the text cells still in the clipboard, convert the clipboard from + // rich data (cells) to plain text and confirm that it gets parsed the same way. + // The cells are still selected, clear them all. + await gu.sendKeys(Key.BACK_SPACE); + await gu.waitForServer(); + assert.deepEqual( + await gu.getVisibleGridCells({rowNums: _.range(1, 9), cols: ['Parsed']}), + arrayRepeat(8, ''), + ); + + // Paste the text cells to the dummy textarea and copy. + await driver.find('#dummyText').click(); + await gu.waitAppFocus(false); + await cb.paste(); + await gu.sendKeys(await gu.selectAllKey()); + await cb.copy(); + await gu.sendKeys(Key.BACK_SPACE); + + // Paste the now plain text and confirm that the resulting data is still the same. + await gu.getCell({col: 'Text', rowNum: 1}).click(); + await gu.waitAppFocus(); + await paste(cb); + await checkGridCells(expected); + + // Check that copying from the Parsed column back into itself doesn't change anything. + await copy(cb, 'Parsed'); + await checkGridCells(expected); + } +} + +function createDummyTextArea() { + const textarea = document.createElement('textarea'); + textarea.style.position = "absolute"; + textarea.style.top = "0"; + textarea.style.height = "2rem"; + textarea.style.width = "16rem"; + textarea.id = 'dummyText'; + window.document.body.appendChild(textarea); +} + +function removeDummyTextArea() { + const textarea = document.getElementById('dummyText'); + if (textarea) { + window.document.body.removeChild(textarea); + } +} diff --git a/test/nbrowser/CopyPaste2.ntest.js b/test/nbrowser/CopyPaste2.ntest.js new file mode 100644 index 00000000..54ba1842 --- /dev/null +++ b/test/nbrowser/CopyPaste2.ntest.js @@ -0,0 +1,329 @@ +/* global window */ + +import { assert, driver } from 'mocha-webdriver'; +import { $, gu, test } from 'test/nbrowser/gristUtil-nbrowser'; + +// Helper that returns the cell text prefixed by "+" if the cell is selected, "-" if not. +async function selText(cell) { + const isSelected = await cell.hasClass('selected'); + const text = await cell.getAttribute('textContent'); + return (isSelected ? "+" : "-") + text; +} + +describe('CopyPaste2.ntest', function() { + const cleanup = test.setupTestSuite(this); + const clipboard = gu.getLockableClipboard(); + + before(async function() { + await gu.supportOldTimeyTestCode(); + await gu.useFixtureDoc(cleanup, "CopyPaste2.grist", true); + }); + + afterEach(function() { + return gu.checkForErrors(); + }); + + it('should highlight correct cells after paste', async function() { + // After paste, the right cells should be highlighted (there was a bug with it when cursor was + // not in the top-left corner of the destination selection). + + // Select a 3x2 rectangle, and check that the data and selection is as we expect. + await gu.clickCell({rowNum: 3, col: 0}); + await gu.sendKeys([$.SHIFT, $.RIGHT], [$.SHIFT, $.DOWN, $.DOWN]); + assert.deepEqual(await gu.getGridValues({rowNums: [3, 4, 5, 6, 7], cols: [0, 1, 2], mapper: selText}), [ + '+A3', '+B3', '-C3', // rowNum 3 + '+A4', '+B4', '-C4', // rowNum 4 + '+A5', '+B5', '-C5', // rowNum 5 + '-A6', '-B6', '-C6', // rowNum 6 + '-A7', '-B7', '-C7', // rowNum 7 + ]); + await clipboard.lockAndPerform(async (cb) => { + await cb.copy(); + + // For destination, select rows 5-6, but with cursor in the bottom-right corner of them. + await gu.clickCell({rowNum: 6, col: 1}); + await gu.sendKeys([$.SHIFT, $.LEFT], [$.SHIFT, $.UP]); + await cb.paste(); + }); + await gu.waitForServer(); + + // The result should have 3 rows selected starting from row 5, col 0. + assert.deepEqual(await gu.getCursorPosition(), {rowNum: 6, col: 1}); + assert.deepEqual(await gu.getGridValues({rowNums: [3, 4, 5, 6, 7], cols: [0, 1, 2], mapper: selText}), [ + '-A3', '-B3', '-C3', // rowNum 3 + '-A4', '-B4', '-C4', // rowNum 4 + '+A3', '+B3', '-C5', // rowNum 5 + '+A4', '+B4', '-C6', // rowNum 6 + '+A5', '+B5', '-C7', // rowNum 7 + ]); + + await gu.undo(); // Go back to initial state. + }); + + it('should allow paste into sorted grids', async function() { + // Sort by column A. + await gu.clickCell({rowNum: 1, col: 0}); + await gu.openColumnMenu('A'); + await $('.grist-floating-menu .test-sort-asc').click(); + await gu.clickCell({rowNum: 1, col: 0}); + + // Check the initial state. Refer to this when trying to understand the results of each step. + assert.deepEqual(await gu.getGridValues({rowNums: [3, 4, 5, 6, 7], cols: [0, 1, 2], mapper: selText}), [ + '-A3', '-B3', '-C3', // rowNum 3 + '-A4', '-B4', '-C4', // rowNum 4 + '-A5', '-B5', '-C5', // rowNum 5 + '-A6', '-B6', '-C6', // rowNum 6 + '-A7', '-B7', '-C7', // rowNum 7 + ]); + + // First test pasting columns B,C: order of rows is not affected. + await gu.clickCell({rowNum: 3, col: 1}); + await gu.sendKeys([$.SHIFT, $.RIGHT], [$.SHIFT, $.DOWN, $.DOWN]); + await clipboard.lockAndPerform(async (cb) => { + await cb.copy(); + await gu.clickCell({rowNum: 5, col: 1}); + await cb.paste(); + }); + await gu.waitForServer(); + + // Check values, and also that the selection is in the paste destination. + assert.deepEqual(await gu.getCursorPosition(), {rowNum: 5, col: 1}); + assert.deepEqual(await gu.getGridValues({rowNums: [3, 4, 5, 6, 7], cols: [0, 1, 2], mapper: selText}), [ + '-A3', '-B3', '-C3', // rowNum 3 + '-A4', '-B4', '-C4', // rowNum 4 + '-A5', '+B3', '+C3', // rowNum 5 + '-A6', '+B4', '+C4', // rowNum 6 + '-A7', '+B5', '+C5', // rowNum 7 + ]); + + await gu.undo(); // Go back to initial state. + + // Now test pasting columns A,B. First a single row: it jumps but cursor should stay in it. + await gu.clickCell({rowNum: 7, col: 0}); + await clipboard.lockAndPerform(async (cb) => { + await cb.copy(); + await gu.clickCell({rowNum: 3, col: 0}); + await cb.paste(); + }); + await gu.waitForServer(); + + // Check values, and also that the selection is in the paste destination. + assert.deepEqual(await gu.getCursorPosition(), {rowNum: 6, col: 0}); + assert.deepEqual(await gu.getGridValues({rowNums: [3, 4, 5, 6, 7], cols: [0, 1, 2], mapper: selText}), [ + '-A4', '-B4', '-C4', // rowNum 3 + '-A5', '-B5', '-C5', // rowNum 4 + '-A6', '-B6', '-C6', // rowNum 5 + '-A7', '-B3', '-C3', // rowNum 6 + '-A7', '-B7', '-C7', // rowNum 7 + ]); + + await gu.undo(); // Go back to initial state. + + // Now multiple rows / columns, including adding records. + await gu.clickCell({rowNum: 3, col: 0}); + await gu.sendKeys([$.SHIFT, $.RIGHT], [$.SHIFT, $.DOWN, $.DOWN]); + await clipboard.lockAndPerform(async (cb) => { + await cb.copy(); + await gu.clickCell({rowNum: 6, col: 0}); + await cb.paste(); + }); + await gu.waitForServer(); + + // Cursor should be in the row which used to be row 6 (has C5 in it); selection is lost + // because rows are no longer contiguous (and better behavior is not yet implemented). + assert.deepEqual(await gu.getCursorPosition(), {rowNum: 4, col: 0}); + assert.deepEqual(await gu.getGridValues({rowNums: [3, 4, 5, 6, 7, 8], cols: [0, 1, 2], mapper: selText}), [ + '-A3', '-B3', '-C3', // rowNum 3 + '-A3', '-B3', '-C6', // rowNum 4 + '-A4', '-B4', '-C4', // rowNum 5 + '-A4', '-B4', '-C7', // rowNum 6 + '-A5', '-B5', '-C5', // rowNum 7 + '-A5', '-B5', '-', // rowNum 8 + ]); + + await gu.undo(); // Go back to initial state. + + // Now B/C column into A/B column, with a row shift. This happens to keep destination rows + // together, so we check that the selection is maintained. + await gu.clickCell({rowNum: 3, col: 1}); + await gu.sendKeys([$.SHIFT, $.RIGHT], [$.SHIFT, $.DOWN]); + await clipboard.lockAndPerform(async (cb) => { + await cb.copy(); + await gu.clickCell({rowNum: 5, col: 0}); + await cb.paste(); + }); + await gu.waitForServer(); + + assert.deepEqual(await gu.getCursorPosition(), {rowNum: 6, col: 0}); + assert.deepEqual(await gu.getGridValues({rowNums: [3, 4, 5, 6, 7], cols: [0, 1, 2], mapper: selText}), [ + '-A3', '-B3', '-C3', // rowNum 3 + '-A4', '-B4', '-C4', // rowNum 4 + '-A7', '-B7', '-C7', // rowNum 5 + '+B3', '+C3', '-C5', // rowNum 6 + '+B4', '+C4', '-C6', // rowNum 7 + ]); + + await gu.undo(); // Go back to initial state. + + // Undo the sorting. + $('.test-section-menu-small-btn-revert').click(); + }); + + it.skip('should copy formatted values to clipboard', async function() { + // Formatted values should be copied to the clipboard as the user sees them (particularly for + // Dates and Reference columns). + // + // FIXME: this test currently fails in headless environments, seemingly due to changes to + // clipboard behavior in recent versions of chromedriver. + + // Select a 3x2 rectangle, and check that the data and selection is as we expect. + await gu.clickCell({rowNum: 3, col: 2}); + await gu.sendKeys([$.SHIFT, $.RIGHT, $.RIGHT, $.RIGHT, $.RIGHT, $.RIGHT], [$.SHIFT, $.DOWN, $.DOWN]); + assert.deepEqual(await gu.getGridValues({rowNums: [1, 2, 3, 4, 5, 6, 7], cols: [2, 3, 4, 5, 6, 7], mapper: selText}), [ + '-C1', '-17.504', '-02/29/16', '-2016-02-29 9:30am', '-April 13, 1743', '-Jefferson', + '-C2', '--3.222', '-03/31/16', '-2016-03-31 9:30am', '-March 16, 1751', '-Madison', + '+C3', '+-4.018', '+04/30/16', '+2016-04-30 9:30am', '+October 30, 1735', '+Adams', + '+C4', '+1829.324', '+05/31/16', '+2016-05-31 9:30am', '+February 22, 1732', '+Washington', + '+C5', '+9402.556', '+06/30/16', '+2016-06-30 9:30am', '+', '+', + '-C6', '-12.000', '-07/31/16', '-2016-07-31 9:30am', '-February 22, 1732', '-Washington', + '-C7', '-0.001', '-08/31/16', '-2016-08-31 9:30am', '-April 13, 1743', '-Jefferson', + ]); + + // Paste data as the text into the open editor of top-left cell, and save. + await clipboard.lockAndPerform(async (cb) => { + await cb.copy(); + await gu.clickCell({rowNum: 1, col: 0}); + await gu.sendKeys($.ENTER, $.SELECT_ALL); + await cb.paste(); + }); + await gu.sendKeys($.ENTER); + await gu.waitForServer(); + + // Note how all values are formatted in the same way as above. + assert.deepEqual(await gu.getCell({rowNum: 1, col: 0}).getAttribute('textContent'), + 'C3\t-4.018\t04/30/16\t2016-04-30 9:30am\tOctober 30, 1735\tAdams\n' + + 'C4\t1829.324\t05/31/16\t2016-05-31 9:30am\tFebruary 22, 1732\tWashington\n' + + 'C5\t9402.556\t06/30/16\t2016-06-30 9:30am\t\t'); + await gu.undo(); // Go back to initial state. + }); + + it.skip('should copy properly in the presence of special characters', async function() { + // If we copy multiple cells (generating text/html to clipboard) and the cells contain special + // html characters (such as angle brackets), those should be escaped. + // + // FIXME: this test currently fails in headless environments, seemingly due to changes to + // clipboard behavior in recent versions of chromedriver. + + await gu.clickCell({rowNum: 1, col: 1}); + await gu.sendKeys($.ENTER, $.SELECT_ALL, " for", [$.SHIFT, $.ENTER], "you & me;", $.ENTER); + await gu.waitForServer(); + + // Add a listener that will save the prepared clipboard data, so that we can examine it. + await driver.executeScript(function() { + window.gristCopyHandler = ev => { + window.copiedClipboardData = {}; + for (let t of ev.clipboardData.types) { + window.copiedClipboardData[t] = ev.clipboardData.getData(t); + } + }; + window.addEventListener('copy', window.gristCopyHandler); + }); + + try { + // Now copy a multi-cell selection including this cell. + await gu.clickCell({rowNum: 1, col: 0}); + await gu.sendKeys([$.SHIFT, $.RIGHT], [$.SHIFT, $.DOWN]); + assert.deepEqual(await gu.getGridValues({rowNums: [1, 2], cols: [0, 1, 2], mapper: selText}), [ + '+A1', '+ for\nyou & me;', '-C1', + '+A2', '+B2', '-C2', + ]); + + await clipboard.lockAndPerform(async (cb) => { + await cb.copy(); + + // Firefox and Chrome actually produce slightly different html, so we just check the part that + // matters: that angle brackets and ampersand got escaped. + assert.include(await driver.executeScript(() => window.copiedClipboardData['text/html']), + 'A1<tag> for\nyou & me;'); + + // Check the contents of text that got copied to the clipboard + assert.equal(await driver.executeScript(() => window.copiedClipboardData['text/plain']), + 'A1\t" for\nyou & me;"\n' + + 'A2\tB2' + ); + + // We can check that we also accept such text correctly by pasting as text inside a cell, and + // then copy-pasting from there. + await gu.clickCell({rowNum: 3, col: 0}); + await gu.sendKeys($.ENTER, $.SELECT_ALL); + await cb.paste(); + }); + await gu.sendKeys($.ENTER); + await gu.waitForServer(); + + await gu.clickCell({rowNum: 3, col: 0}); + await gu.sendKeys($.ENTER, $.SELECT_ALL); + await clipboard.lockAndPerform(async (cb) => { + await cb.copy(); + await gu.sendKeys($.ESCAPE); + await gu.clickCell({rowNum: 4, col: 0}); + await cb.paste(); + }); + + await gu.waitForServer(); + assert.deepEqual(await gu.getGridValues({rowNums: [1, 2, 3, 4, 5], cols: [0, 1, 2], mapper: selText}), [ + '-A1', '- for\nyou & me;', '-C1', + '-A2', '-B2', '-C2', + '-A1\t" for\nyou & me;"\nA2\tB2', '-B3', '-C3', + '+A1', '+ for\nyou & me;', '-C4', + '+A2', '+B2', '-C5', + ]); + + await gu.undo(3); // Go back to initial state. + } finally { + await driver.executeScript(function() { + window.removeEventListener('copy', window.gristCopyHandler); + }); + } + }); + + it('should paste correctly when values contain commas', async function() { + // When pasting, split only on tabs, not on commas. (We used to split on both, or guess what + // to split on, which resulted in unexpected and unpleasant surprises when a legitimate value + // contained a comma.) + + // Create a value with commas. + await gu.clickCell({rowNum: 1, col: 0}); + await gu.sendKeys($.ENTER, $.SELECT_ALL, "this,is,a,test", $.ENTER); + await gu.waitForServer(); + + // Copy a single value, and paste to another cell. + await gu.clickCell({rowNum: 1, col: 0}); + await clipboard.lockAndPerform(async (cb) => { + await cb.copy(); + await gu.clickCell({rowNum: 2, col: 0}); + await cb.paste(); + }); + await gu.waitForServer(); + assert.deepEqual(await gu.getGridValues({rowNums: [1, 2], cols: [0, 1, 2], mapper: selText}), [ + '-this,is,a,test', '-B1', '-C1', + '-this,is,a,test', '-B2', '-C2', + ]); + + // Now copy multiple values, and paste to other cells. + await gu.sendKeys([$.SHIFT, $.UP], [$.SHIFT, $.RIGHT]); + await clipboard.lockAndPerform(async (cb) => { + await cb.copy(); + await gu.clickCell({rowNum: 1, col: 1}); + await cb.paste(); + }); + await gu.waitForServer(); + + assert.deepEqual(await gu.getGridValues({rowNums: [1, 2], cols: [0, 1, 2], mapper: selText}), [ + '-this,is,a,test', '+this,is,a,test', '+B1', + '-this,is,a,test', '+this,is,a,test', '+B2', + ]); + + await gu.undo(3); // Go back to initial state. + }); +}); diff --git a/test/nbrowser/CopyPasteColumnOptions.ts b/test/nbrowser/CopyPasteColumnOptions.ts new file mode 100644 index 00000000..baded957 --- /dev/null +++ b/test/nbrowser/CopyPasteColumnOptions.ts @@ -0,0 +1,363 @@ +/** + * Test for copy-pasting from a Grist column into a blank column, which should copy the options. + */ +import {safeJsonParse} from 'app/common/gutil'; +import {GristObjCode} from 'app/plugin/GristData'; +import {assert} from 'mocha-webdriver'; +import * as gu from 'test/nbrowser/gristUtils'; +import {setupTestSuite} from 'test/nbrowser/testUtils'; + +describe('CopyPasteColumnOptions', function() { + this.timeout(10000); + const cleanup = setupTestSuite(); + const clipboard = gu.getLockableClipboard(); + afterEach(() => gu.checkForErrors()); + gu.bigScreen(); + + it('should copy column options into blank columns', async function() { + const session = await gu.session().login(); + const doc = await session.tempDoc(cleanup, 'CopyOptions.grist'); + const api = session.createHomeApi().getDocAPI(doc.id); + const data1 = await api.getRows("Table1"); + const data2 = await api.getRows("Table2"); + + assert.deepEqual(data1, { + "id": [1], + "manualSort": [1], + "A": [1041465600], + "B": [1044057600], + "C": [1], + "D": [[GristObjCode.List, 1, 1]], + "E": ["01/02/03"], + "F": [[GristObjCode.List, "01/02/03"]], + "G": ["01/02/03"], + "gristHelper_Display": [[GristObjCode.Date, 1041465600]], + "gristHelper_Display2": [[GristObjCode.List, [GristObjCode.Date, 1044057600], [GristObjCode.Date, 1044057600]]], + "gristHelper_ConditionalRule": [true], + }); + + // Initially Table2 is completely empty, all the columns are blank and of type Any + assert.deepEqual(data2, { + "id": [], + "manualSort": [], + "A": [], + "B": [], + "C2": [], + "D2": [], + "E": [], + "F": [], + "G": [], + }); + + // Copy all the data from Table1 to Table2, which will copy the column options + await gu.getCell({section: 'TABLE1', col: 0, rowNum: 1}).click(); + await gu.sendKeys(await gu.selectAllKey()); + await clipboard.lockAndPerform(async (cb) => { + await cb.copy(); + await gu.getCell({section: 'TABLE2', col: 0, rowNum: 1}).click(); + await cb.paste(); + }); + await gu.waitForServer(); + + // Now Table2 contains essentially the same data as Table1 + // Table2 just has slightly different column names to test display formulas, + // and conditional formatting is not copied at the moment. + data1.C2 = data1.C; + data1.D2 = data1.D; + delete data1.C; + delete data1.D; + delete data1.gristHelper_ConditionalRule; + // Actual difference: G is a Text column, so its type was guessed as Date and the string was parsed + data1.G = [981158400]; + assert.deepEqual(await api.getRows("Table2"), data1); + + // Second check that the data is the same, and also that it's formatted the same + const cols1 = ["A", "B", "C", "D", "E", "F", "G"]; + const cols2 = ["A", "B", "C2", "D2", "E", "F", "G"]; + assert.deepEqual( + await gu.getVisibleGridCells({cols: cols1, rowNums: [1], section: "TABLE1"}), + await gu.getVisibleGridCells({cols: cols2, rowNums: [1], section: "TABLE2"}), + ); + + // Check that the column options are essentially the same in both tables + const cols = await api.getRecords("_grist_Tables_column"); + const cleanCols = cols.map( + ({ + id, + fields: { + parentId, + colId, + type, + visibleCol, + displayCol, + rules, + widgetOptions, + formula + } + }) => ({ + id, + parentId, + colId, + type, + visibleCol, + displayCol, + rules, + formula, + widgetOptions: safeJsonParse(widgetOptions as string, ""), + })); + + assert.deepEqual(cleanCols, [ + { + "id": 1, + "parentId": 1, + "colId": "manualSort", + "type": "ManualSortPos", + "visibleCol": 0, + "displayCol": 0, + "rules": null, + "formula": "", + "widgetOptions": "" + }, { + "id": 2, + "parentId": 1, + "colId": "A", + "type": "Date", + "visibleCol": 0, + "displayCol": 0, + "rules": null, + "formula": "", + "widgetOptions": { + "widget": "TextBox", + "dateFormat": "MM/DD/YY", + "isCustomDateFormat": false, + "alignment": "left" + } + }, { + "id": 3, + "parentId": 1, + "colId": "B", + "type": "Date", + "visibleCol": 0, + "displayCol": 0, + "rules": null, + "formula": "", + "widgetOptions": { + "widget": "TextBox", + "dateFormat": "DD/MM/YY", + "isCustomDateFormat": true, + "alignment": "center" + } + }, { + "id": 4, + "parentId": 1, + "colId": "C", + "type": "Ref:Table1", + "visibleCol": 2, + "displayCol": 5, + "rules": null, + "formula": "", + "widgetOptions": {"widget": "Reference", "alignment": "left", "fillColor": "#FECC81"} + }, { + "id": 5, + "parentId": 1, + "colId": "gristHelper_Display", + "type": "Any", + "visibleCol": 0, + "displayCol": 0, + "rules": null, + "formula": "$C.A", + "widgetOptions": "" + }, { + "id": 6, + "parentId": 1, + "colId": "D", + "type": "RefList:Table1", + "visibleCol": 3, + "displayCol": 7, + "rules": null, + "formula": "", + "widgetOptions": {"widget": "Reference", "alignment": "left", "rulesOptions": [], "wrap": true} + }, { + "id": 7, + "parentId": 1, + "colId": "gristHelper_Display2", + "type": "Any", + "visibleCol": 0, + "displayCol": 0, + "rules": null, + "formula": "$D.B", + "widgetOptions": "" + }, { + "id": 8, + "parentId": 1, + "colId": "E", + "type": "Choice", + "visibleCol": 0, + "displayCol": 0, + "rules": null, + "formula": "", + "widgetOptions": {"widget": "TextBox", "alignment": "left", "choices": ["01/02/03"], "choiceOptions": {}} + }, { + "id": 9, + "parentId": 1, + "colId": "F", + "type": "ChoiceList", + "visibleCol": 0, + "displayCol": 0, + "rules": [GristObjCode.List, 21], // Not copied into the new table + "formula": "", + "widgetOptions": { + "widget": "TextBox", + "choices": ["01/02/03", "foo"], + "choiceOptions": {}, + "alignment": "left", + "rulesOptions": [{"fillColor": "#BC77FC", "textColor": "#000000"}] // Not copied into the new table + } + }, { + "id": 10, + "parentId": 1, + "colId": "G", + "type": "Text", + "visibleCol": 0, + "displayCol": 0, + "rules": null, + "formula": "", + "widgetOptions": {"widget": "TextBox", "alignment": "left", "rulesOptions": []} + }, + + ///////////////// + ///// Table2 starts here. Most of the column options are now the same. + ///////////////// + { + "id": 13, + "parentId": 2, + "colId": "manualSort", + "type": "ManualSortPos", + "visibleCol": 0, + "displayCol": 0, + "rules": null, + "formula": "", + "widgetOptions": "" + }, { + "id": 14, + "parentId": 2, + "colId": "A", + "type": "Date", + "visibleCol": 0, + "displayCol": 0, + "rules": null, + "formula": "", + "widgetOptions": { + "widget": "TextBox", + "dateFormat": "MM/DD/YY", + "isCustomDateFormat": false, + "alignment": "left" + } + }, { + "id": 15, + "parentId": 2, + "colId": "B", + "type": "Date", + "visibleCol": 0, + "displayCol": 0, + "rules": null, + "formula": "", + "widgetOptions": { + "widget": "TextBox", + "dateFormat": "DD/MM/YY", + "isCustomDateFormat": true, + "alignment": "center" + } + }, { + "id": 16, + "parentId": 2, + "colId": "C2", + "type": "Ref:Table1", + "visibleCol": 2, + "displayCol": 22, + "rules": null, + "formula": "", + "widgetOptions": {"widget": "Reference", "alignment": "left", "fillColor": "#FECC81"} + }, { + "id": 17, + "parentId": 2, + "colId": "D2", + "type": "RefList:Table1", + "visibleCol": 3, + "displayCol": 23, + "rules": null, + "formula": "", + "widgetOptions": {"widget": "Reference", "alignment": "left", "wrap": true} + }, { + "id": 18, + "parentId": 2, + "colId": "E", + "type": "Choice", + "visibleCol": 0, + "displayCol": 0, + "rules": null, + "formula": "", + "widgetOptions": {"widget": "TextBox", "alignment": "left", "choices": ["01/02/03"], "choiceOptions": {}} + }, { + "id": 19, + "parentId": 2, + "colId": "F", + "type": "ChoiceList", + "visibleCol": 0, + "displayCol": 0, + "rules": null, + "formula": "", + "widgetOptions": {"widget": "TextBox", "choices": ["01/02/03", "foo"], "choiceOptions": {}, "alignment": "left"} + }, { + // Actual difference: the original 'G' is a Text column, so in the new column the type was guessed as Date + "id": 20, + "parentId": 2, + "colId": "G", + "type": "Date", + "visibleCol": 0, + "displayCol": 0, + "rules": null, + "formula": "", + "widgetOptions": { + "timeFormat": "", + "isCustomTimeFormat": true, + "isCustomDateFormat": true, + "dateFormat": "YY/MM/DD" + } + }, { + "id": 21, + // This is in Table1, it's here because it was created in the fixture after Table2 + // No similar column is in Table2 because conditional formatting is not copied + "parentId": 1, + "colId": "gristHelper_ConditionalRule", + "type": "Any", + "visibleCol": 0, + "displayCol": 0, + "rules": null, + "formula": "True", + "widgetOptions": "" + }, { + "id": 22, + "parentId": 2, + "colId": "gristHelper_Display", + "type": "Any", + "visibleCol": 0, + "displayCol": 0, + "rules": null, + "formula": "$C2.A", // Correctly 'renamed' from $C.A + "widgetOptions": "" + }, { + "id": 23, + "parentId": 2, + "colId": "gristHelper_Display2", + "type": "Any", + "visibleCol": 0, + "displayCol": 0, + "rules": null, + "formula": "$D2.B", // Correctly 'renamed' from $D.A + "widgetOptions": "" + }] + ); + }); + +}); diff --git a/test/nbrowser/CopyPasteLinked.ts b/test/nbrowser/CopyPasteLinked.ts new file mode 100644 index 00000000..0bf9dbc9 --- /dev/null +++ b/test/nbrowser/CopyPasteLinked.ts @@ -0,0 +1,64 @@ +/** + * Test for pasting into a linked GridView. + * + * In particular, when multiple rows are selected in GridView, on switching to a different linked + * record, the selection should be cleared, or else paste will misbehave. + */ +import {assert, Key, WebElement} from 'mocha-webdriver'; +import * as gu from 'test/nbrowser/gristUtils'; +import {setupTestSuite} from 'test/nbrowser/testUtils'; + +describe('CopyPasteLinked', function() { + this.timeout(30000); + const cleanup = setupTestSuite(); + const clipboard = gu.getLockableClipboard(); + + it('should clear internal selection when link record changes', async function() { + const mainSession = await gu.session().login(); + await mainSession.tempDoc(cleanup, 'Landlord.grist'); + await gu.getPageItem(/Current Signers/).click(); + await gu.waitForServer(); + + let cell: WebElement; + + // Select a cell. + cell = await gu.getCell({section: 'Tenants', col: 'Tenant', rowNum: 1}); + await cell.click(); + assert.equal(await cell.getText(), 'John Malik'); + + await clipboard.lockAndPerform(async (cb) => { + // Copy the cell's value to the clipboard. + await cb.copy(); + + // Now select multiple cells. + await gu.sendKeys(Key.chord(Key.SHIFT, Key.DOWN), Key.chord(Key.SHIFT, Key.DOWN)); + + // Check that 3 cells are indeed selected. + assert.deepEqual(await gu.getVisibleGridCells({col: 'Tenant', rowNums: [1, 2, 3, 4], + mapper: (el) => el.matches('.selected')}), + [true, true, true, false]); + + // Switch to a different Apartments row that drives the filtering in the Tenants section. + await gu.getCell({section: 'Apartments', col: 0, rowNum: 2}).click(); + cell = await gu.getCell({section: 'Tenants', col: 'Tenant', rowNum: 1}); + await cell.click(); + assert.equal(await cell.getText(), 'Fred Brown'); + + // Paste the copied value. It doesn't work reliably in a test, so try until it works. (The + // reasons seems to be that 'body' has focus briefly, rather than Clipboard component.) + await gu.waitAppFocus(); + await cb.paste(); + }); + await gu.waitForServer(); + + // Check that only one value was copied, and that there are not multiple cells selected. + assert.deepEqual(await gu.getVisibleGridCells({col: 'Tenant', rowNums: [1, 2, 3, 4]}), + ['John Malik', 'Fred Brown', 'Susan Sharp', 'Owen Sharp']); + assert.deepEqual(await gu.getVisibleGridCells({col: 'Tenant', rowNums: [1, 2, 3, 4], + mapper: (el) => el.matches('.selected')}), + [false, false, false, false]); + + await gu.checkForErrors(); + await gu.undo(); + }); +}); diff --git a/test/nbrowser/CustomWidgetsConfig.ts b/test/nbrowser/CustomWidgetsConfig.ts index 35232ffc..528bb398 100644 --- a/test/nbrowser/CustomWidgetsConfig.ts +++ b/test/nbrowser/CustomWidgetsConfig.ts @@ -194,6 +194,13 @@ describe('CustomWidgetsConfig', function () { // Wait for a frame. public async waitForFrame() { await driver.findWait(`iframe.test-custom-widget-ready`, 1000); + await driver.wait(async () => await driver.find('iframe').isDisplayed(), 1000); + await widget.waitForPendingRequests(); + } + public async waitForPendingRequests() { + await this._inWidgetIframe(async () => { + await driver.executeScript('grist.testWaitForPendingRequests();'); + }); } public async content() { return await this._read('body'); @@ -232,13 +239,12 @@ describe('CustomWidgetsConfig', function () { } // Wait for the onOptions event, and return its value. public async onOptions() { - const iframe = driver.find('iframe'); - await driver.switchTo().frame(iframe); - // Wait for options to get filled, initially this div is empty, - // as first message it should get at least null as an options. - await driver.wait(async () => await driver.find('#onOptions').getText(), 3000); - const text = await driver.find('#onOptions').getText(); - await driver.switchTo().defaultContent(); + const text = await this._inWidgetIframe(async () => { + // Wait for options to get filled, initially this div is empty, + // as first message it should get at least null as an options. + await driver.wait(async () => await driver.find('#onOptions').getText(), 3000); + return await driver.find('#onOptions').getText(); + }); return JSON.parse(text); } public async wasConfigureCalled() { @@ -308,11 +314,15 @@ describe('CustomWidgetsConfig', function () { } private async _read(selector: string) { + return this._inWidgetIframe(() => driver.find(selector).getText()); + } + + private async _inWidgetIframe(callback: () => Promise) { const iframe = driver.find('iframe'); await driver.switchTo().frame(iframe); - const text = await driver.find(selector).getText(); + const retVal = await callback(); await driver.switchTo().defaultContent(); - return text; + return retVal; } } // Rpc for main widget (Custom Widget). @@ -337,7 +347,9 @@ describe('CustomWidgetsConfig', function () { }) ); + await widget.waitForFrame(); await gu.acceptAccessRequest(); + await widget.waitForPendingRequests(); // Get the drop for M2 mappings. const mappingsForM2 = () => driver.find(pickerDrop('M2')); @@ -387,7 +399,9 @@ describe('CustomWidgetsConfig', function () { }) ); + await widget.waitForFrame(); await gu.acceptAccessRequest(); + await widget.waitForPendingRequests(); // Get the drop for M2 mappings. const mappingsForM2 = () => driver.find(pickerDrop('M2')); @@ -428,6 +442,7 @@ describe('CustomWidgetsConfig', function () { await clickOption(COLUMN_WIDGET); await widget.waitForFrame(); await gu.acceptAccessRequest(); + await widget.waitForPendingRequests(); // Visible columns section should be hidden. assert.isFalse(await driver.find('.test-vfc-visible-fields-select-all').isPresent()); // Record event should be fired. @@ -444,7 +459,7 @@ describe('CustomWidgetsConfig', function () { await toggleDrop(pickerDrop('Column')); assert.deepEqual(await getOptions(), ['A']); await clickOption('A'); - await gu.waitForServer(); + await widget.waitForPendingRequests(); // Widget should receive mappings assert.deepEqual(await widget.onRecordsMappings(), {Column: 'A'}); await revert(); @@ -462,9 +477,10 @@ describe('CustomWidgetsConfig', function () { requiredAccess: 'read table', }) ); - await gu.acceptAccessRequest(); - const empty = {M1: null, M2: null, M3: null, M4: null}; await widget.waitForFrame(); + await gu.acceptAccessRequest(); + await widget.waitForPendingRequests(); + // Mappings should be empty assert.isNull(await widget.onRecordsMappings()); // We should see 4 pickers assert.isTrue(await driver.find(pickerLabel('M1')).isPresent()); @@ -481,29 +497,29 @@ describe('CustomWidgetsConfig', function () { assert.equal(await driver.find(pickerDrop('M2')).getText(), 'Pick a column'); assert.equal(await driver.find(pickerDrop('M3')).getText(), 'Pick a column'); assert.equal(await driver.find(pickerDrop('M4')).getText(), 'Pick a text column'); - // Mappings should be empty - assert.isNull(await widget.onRecordsMappings()); // Should be able to select column A for all options await toggleDrop(pickerDrop('M1')); await clickOption('A'); - await gu.waitForServer(); + await widget.waitForPendingRequests(); + const empty = {M1: null, M2: null, M3: null, M4: null}; assert.deepEqual(await widget.onRecordsMappings(), {... empty, M1: 'A'}); await toggleDrop(pickerDrop('M2')); await clickOption('A'); - await gu.waitForServer(); + await widget.waitForPendingRequests(); assert.deepEqual(await widget.onRecordsMappings(), {... empty, M1: 'A', M2: 'A'}); await toggleDrop(pickerDrop('M3')); await clickOption('A'); - await gu.waitForServer(); + await widget.waitForPendingRequests(); assert.deepEqual(await widget.onRecordsMappings(), {... empty, M1: 'A', M2: 'A', M3: 'A'}); await toggleDrop(pickerDrop('M4')); await clickOption('A'); - await gu.waitForServer(); + await widget.waitForPendingRequests(); assert.deepEqual(await widget.onRecordsMappings(), {M1: 'A', M2: 'A', M3: 'A', M4: 'A'}); // Single record should also receive update. assert.deepEqual(await widget.onRecordMappings(), {M1: 'A', M2: 'A', M3: 'A', M4: 'A'}); // Undo should revert mappings - there should be only 3 operations to revert to first mapping. await gu.undo(3); + await widget.waitForPendingRequests(); assert.deepEqual(await widget.onRecordsMappings(), {... empty, M1: 'A'}); // Add another columns, numeric B and any C. await gu.selectSectionByTitle('Table'); @@ -519,6 +535,7 @@ describe('CustomWidgetsConfig', function () { assert.deepEqual(await getOptions(), ['A', 'C']); await toggleDrop(pickerDrop('M1')); await clickOption('B'); + await widget.waitForPendingRequests(); assert.deepEqual(await widget.onRecordsMappings(), {...empty, M1: 'B'}); await revert(); }); @@ -528,7 +545,9 @@ describe('CustomWidgetsConfig', function () { await toggleWidgetMenu(); await clickOption(COLUMN_WIDGET); + await widget.waitForFrame(); await gu.acceptAccessRequest(); + await widget.waitForPendingRequests(); // Make sure columns are there to pick. @@ -540,7 +559,6 @@ describe('CustomWidgetsConfig', function () { // Pick first column await toggleDrop(pickerDrop('Column')); await clickOption('A'); - await gu.waitForServer(); // Now change to a widget without columns await toggleWidgetMenu(); @@ -560,7 +578,9 @@ describe('CustomWidgetsConfig', function () { // Now go back to the widget with mappings. await toggleWidgetMenu(); await clickOption(COLUMN_WIDGET); + await widget.waitForFrame(); await gu.acceptAccessRequest(); + await widget.waitForPendingRequests(); assert.equal(await driver.find(pickerDrop('Column')).getText(), 'Pick a column'); assert.isFalse(await driver.find('.test-vfc-visible-fields-select-all').isPresent()); assert.isTrue(await driver.find('.test-config-widget-label-for-Column').isPresent()); @@ -580,9 +600,8 @@ describe('CustomWidgetsConfig', function () { requiredAccess: 'read table', }) ); - await gu.acceptAccessRequest(); - const empty = {M1: [], M2: []}; await widget.waitForFrame(); + await gu.acceptAccessRequest(); // Add some columns, numeric B and any C. await gu.selectSectionByTitle('Table'); await gu.addColumn('B'); @@ -590,6 +609,7 @@ describe('CustomWidgetsConfig', function () { await gu.enterCell('99'); await gu.addColumn('C'); await gu.selectSectionByTitle('Widget'); + await widget.waitForPendingRequests(); // Make sure we have no mappings assert.deepEqual(await widget.onRecordsMappings(), null); // Map all columns to M1 @@ -600,6 +620,8 @@ describe('CustomWidgetsConfig', function () { await clickMenuItem('B'); await click(pickerAdd('M1')); await clickMenuItem('C'); + await widget.waitForPendingRequests(); + const empty = {M1: [], M2: []}; assert.deepEqual(await widget.onRecordsMappings(), {...empty, M1: ['A', 'B', 'C']}); // Map A and C to M2 await click(pickerAdd('M2')); @@ -609,6 +631,7 @@ describe('CustomWidgetsConfig', function () { await clickMenuItem('A'); await click(pickerAdd('M2')); await clickMenuItem('C'); + await widget.waitForPendingRequests(); assert.deepEqual(await widget.onRecordsMappings(), {M1: ['A', 'B', 'C'], M2: ['A', 'C']}); function dragItem(column: string, item: string) { return driver.findContent(`.test-config-widget-map-list-for-${column} .kf_draggable`, item); @@ -623,6 +646,7 @@ describe('CustomWidgetsConfig', function () { .release() ); await gu.waitForServer(); + await widget.waitForPendingRequests(); assert.deepEqual(await widget.onRecordsMappings(), {M1: ['B', 'C', 'A'], M2: ['A', 'C']}); // Should support removing const removeButton = (column: string, item: string) => { @@ -630,9 +654,11 @@ describe('CustomWidgetsConfig', function () { }; await removeButton('M1', 'B').click(); await gu.waitForServer(); + await widget.waitForPendingRequests(); assert.deepEqual(await widget.onRecordsMappings(), {M1: ['C', 'A'], M2: ['A', 'C']}); // Should undo removing await gu.undo(); + await widget.waitForPendingRequests(); assert.deepEqual(await widget.onRecordsMappings(), {M1: ['B', 'C', 'A'], M2: ['A', 'C']}); await removeButton('M1', 'B').click(); await gu.waitForServer(); @@ -640,6 +666,7 @@ describe('CustomWidgetsConfig', function () { await gu.waitForServer(); await removeButton('M2', 'C').click(); await gu.waitForServer(); + await widget.waitForPendingRequests(); assert.deepEqual(await widget.onRecordsMappings(), {M1: ['A'], M2: ['A']}); await revert(); }); @@ -657,8 +684,8 @@ describe('CustomWidgetsConfig', function () { requiredAccess: 'read table', }) ); - await gu.acceptAccessRequest(); await widget.waitForFrame(); + await gu.acceptAccessRequest(); // Add B=Date, C=DateTime, D=Numeric await gu.sendActions([ ['AddVisibleColumn', 'Table1', 'B', {type: 'Any'}], @@ -670,6 +697,7 @@ describe('CustomWidgetsConfig', function () { ]); await gu.selectSectionByTitle('Widget'); + await widget.waitForPendingRequests(); // Make sure we have no mappings assert.deepEqual(await widget.onRecordsMappings(), null); // Now see what we are offered for M1. @@ -719,8 +747,8 @@ describe('CustomWidgetsConfig', function () { requiredAccess: 'read table', }) ); - await gu.acceptAccessRequest(); await widget.waitForFrame(); + await gu.acceptAccessRequest(); await gu.sendActions([ ['AddVisibleColumn', 'Table1', 'Any', {type: 'Any'}], ['AddVisibleColumn', 'Table1', 'Date', {type: 'Date'}], @@ -728,6 +756,8 @@ describe('CustomWidgetsConfig', function () { ]); await gu.selectSectionByTitle('Widget'); + await widget.waitForPendingRequests(); + // Make sure we have no mappings assert.deepEqual(await widget.onRecordsMappings(), null); @@ -759,20 +789,22 @@ describe('CustomWidgetsConfig', function () { }) ); + await widget.waitForFrame(); + await gu.acceptAccessRequest(); + const widgetOptions = { choices: ['A'], choiceOptions: {A: {textColor: 'red'}} }; - await gu.sendActions([ ['AddVisibleColumn', 'Table1', 'Choice', {type: 'Choice', widgetOptions: JSON.stringify(widgetOptions)}] ]); - await gu.acceptAccessRequest(); - await widget.waitForFrame(); - await gu.selectSectionByTitle('Widget'); + await widget.waitForPendingRequests(); + await toggleDrop(pickerDrop('Choice')); await clickOption('Choice'); + await widget.waitForPendingRequests(); // Clear logs await widget.clearLog(); @@ -803,22 +835,25 @@ describe('CustomWidgetsConfig', function () { requiredAccess: 'read table', }) ); - await gu.acceptAccessRequest(); await widget.waitForFrame(); + await gu.acceptAccessRequest(); // Add some columns, to remove later await gu.selectSectionByTitle('Table'); await gu.addColumn('B'); await gu.addColumn('C'); await gu.selectSectionByTitle('Widget'); + await widget.waitForPendingRequests(); // Make sure we have no mappings assert.deepEqual(await widget.onRecordsMappings(), null); // Map B to M1 await toggleDrop(pickerDrop('M1')); await clickOption('B'); + await widget.waitForPendingRequests(); // Map all columns to M2 for (const col of ['A', 'B', 'C']) { await click(pickerAdd('M2')); await clickMenuItem(col); + await widget.waitForPendingRequests(); } assert.deepEqual(await widget.onRecordsMappings(), {M1: 'B', M2: ['A', 'B', 'C']}); assert.deepEqual(await widget.onRecords(), [ @@ -830,10 +865,12 @@ describe('CustomWidgetsConfig', function () { await gu.selectSectionByTitle('Table'); await gu.openColumnMenu(col, 'Delete column'); await gu.waitForServer(); + await widget.waitForPendingRequests(); await gu.selectSectionByTitle('Widget'); }; // Remove B column await removeColumn('B'); + await widget.waitForPendingRequests(); // Mappings should be updated assert.deepEqual(await widget.onRecordsMappings(), {M1: null, M2: ['A', 'C']}); // Records should not have B column @@ -848,6 +885,7 @@ describe('CustomWidgetsConfig', function () { await gu.selectSectionByTitle('Table'); await gu.addColumn('B'); await gu.selectSectionByTitle('Widget'); + await widget.waitForPendingRequests(); // Adding the same column should not add it to mappings or records (as this is a new Id) assert.deepEqual(await widget.onRecordsMappings(), {M1: null, M2: ['A', 'C']}); assert.deepEqual(await widget.onRecords(), [ @@ -861,9 +899,11 @@ describe('CustomWidgetsConfig', function () { // Make sure it is there to select. assert.deepEqual(await getOptions(), ['A', 'C', 'B']); await clickOption('B'); + await widget.waitForPendingRequests(); await click(pickerAdd('M2')); assert.deepEqual(await getMenuOptions(), ['B']); // multiple selection will only show not selected columns await clickMenuItem('B'); + await widget.waitForPendingRequests(); assert.deepEqual(await widget.onRecordsMappings(), {M1: 'B', M2: ['A', 'C', 'B']}); assert.deepEqual(await widget.onRecords(), [ {id: 1, B: null, A: 'A', C: null}, @@ -884,8 +924,9 @@ describe('CustomWidgetsConfig', function () { requiredAccess: 'read table', }) ); - await gu.acceptAccessRequest(); await widget.waitForFrame(); + await gu.acceptAccessRequest(); + await widget.waitForPendingRequests(); assert.deepEqual(await widget.onRecordsMappings(), null); assert.deepEqual(await widget.onRecords(), [ {id: 1, A: 'A'}, @@ -910,8 +951,10 @@ describe('CustomWidgetsConfig', function () { await gu.setType(/Numeric/); await gu.selectSectionByTitle('Widget'); await driver.find(".test-right-tab-pagewidget").click(); + await widget.waitForPendingRequests(); // Drop should be empty, - assert.equal(await driver.find(pickerDrop("M1")).getText(), "No text columns in table."); + await driver.wait(async () => + await driver.find(pickerDrop("M1")).getText() == "No text columns in table.", 1000); assert.isEmpty(await getListItems("M2")); // And drop is disabled. assert.isTrue(await driver.find(pickerDrop("M1")).matches(".test-config-widget-disabled")); @@ -1124,25 +1167,30 @@ describe('CustomWidgetsConfig', function () { // Select widget without request await toggleWidgetMenu(); await clickOption(NORMAL_WIDGET); + await widget.waitForFrame(); assert.isFalse(await gu.hasAccessPrompt()); assert.equal(await gu.widgetAccess(), AccessLevel.none); assert.equal(await widget.access(), AccessLevel.none); // Select widget that requests read access. await toggleWidgetMenu(); await clickOption(READ_WIDGET); + await widget.waitForFrame(); assert.isTrue(await gu.hasAccessPrompt()); assert.equal(await gu.widgetAccess(), AccessLevel.none); assert.equal(await widget.access(), AccessLevel.none); await gu.acceptAccessRequest(); + await widget.waitForPendingRequests(); assert.equal(await gu.widgetAccess(), AccessLevel.read_table); assert.equal(await widget.access(), AccessLevel.read_table); // Select widget that requests full access. await toggleWidgetMenu(); await clickOption(FULL_WIDGET); + await widget.waitForFrame(); assert.isTrue(await gu.hasAccessPrompt()); assert.equal(await gu.widgetAccess(), AccessLevel.none); assert.equal(await widget.access(), AccessLevel.none); await gu.acceptAccessRequest(); + await widget.waitForPendingRequests(); assert.equal(await gu.widgetAccess(), AccessLevel.full); assert.equal(await widget.access(), AccessLevel.full); await gu.undo(5); diff --git a/test/nbrowser/DetailView.ntest.js b/test/nbrowser/DetailView.ntest.js index 904b0fdd..327ed689 100644 --- a/test/nbrowser/DetailView.ntest.js +++ b/test/nbrowser/DetailView.ntest.js @@ -3,6 +3,7 @@ import { $, gu, test } from 'test/nbrowser/gristUtil-nbrowser'; describe("DetailView.ntest", function () { const cleanup = test.setupTestSuite(this); + const clipboard = gu.getLockableClipboard(); gu.bigScreen(); before(async function() { @@ -110,8 +111,6 @@ describe("DetailView.ntest", function () { it('should include an add record row', async function() { // Should include an add record row which works in card view and detail view. // Check that adding 'Jurassic Park' to the card view add record row adds it as a row. - // await gu.selectSectionByTitle("Performances detail"); - //await gu.sendKeys([$.MOD, $.DOWN]); await $('.g_record_detail:nth-child(14) .field_clip').eq(1).wait().click(); await gu.sendKeys('Jurassic Park', $.ENTER); await gu.waitForServer(); @@ -130,11 +129,14 @@ describe("DetailView.ntest", function () { // Should allow pasting into the add record row. await gu.getDetailCell('Actor', 1).click(); - await gu.sendKeys($.COPY); - await $('.detail-add-btn').click(); - // Paste '100' into the last field of the row and check that it is added as its own row. - await gu.getDetailCell('Character', 1).click(); - await gu.sendKeys($.PASTE); + await clipboard.lockAndPerform(async (cb) => { + await cb.copy(); + await $('.detail-add-btn').click(); + await gu.waitForServer(); + // Paste '100' into the last field of the row and check that it is added as its own row. + await gu.getDetailCell('Character', 1).click(); + await cb.paste(); + }); await gu.waitForServer(); assert.deepEqual(await gu.getDetailCell('Character', 1).text(), '100'); diff --git a/test/nbrowser/DocTutorial.ts b/test/nbrowser/DocTutorial.ts index f52e34ba..70d1cc98 100644 --- a/test/nbrowser/DocTutorial.ts +++ b/test/nbrowser/DocTutorial.ts @@ -7,7 +7,6 @@ import {EnvironmentSnapshot} from 'test/server/testUtils'; describe('DocTutorial', function () { this.timeout(60000); - setupTestSuite(); gu.bigScreen(); @@ -196,8 +195,10 @@ describe('DocTutorial', function () { const windowWidth: any = await driver.executeScript('return window.innerWidth'); assert.equal(dims.y + dims.height, windowHeight - 16); - // Now move it offscreen as low as possible. - await move({y: windowHeight - dims.y - 10}); + // Now we'll test moving the window outside the viewport. Selenium throws when + // the mouse exceeds the bounds of the viewport, so we can't test every scenario. + // Start by moving the window as low as possible. + await move({y: windowHeight - dims.y - 32}); // Make sure it is still visible. dims = await driver.find('.test-floating-popup-window').getRect(); @@ -213,71 +214,33 @@ describe('DocTutorial', function () { assert.equal(dims.x, windowWidth - 32 * 4); // Now move it to the left as far as possible. - await move({x: -3000}); + await move({x: -windowWidth + 128}); // Make sure it is still visible. dims = await driver.find('.test-floating-popup-window').getRect(); - assert.isBelow(dims.x, 0); + assert.equal(dims.x, 0); assert.isAbove(dims.x + dims.width, 30); - const miniButton = driver.find(".test-floating-popup-minimize-maximize"); - // Now move it back, but this time manually as the move handle is off screen. - await driver.withActions((a) => a - .move({origin: miniButton }) - .press() - .move({origin: miniButton, x: Math.abs(dims.x) + 20}) - .release() - ); - - // Maximize it (it was minimized as we used the button to move it). - await driver.find(".test-floating-popup-minimize-maximize").click(); - // Now move it to the top as far as possible. // Move it a little right, so that we don't end up on the logo. Driver is clicking logo sometimes. - await move({y: -windowHeight, x: 100}); + await move({y: -windowHeight + 16, x: 100}); // Make sure it is still visible. dims = await driver.find('.test-floating-popup-window').getRect(); assert.equal(dims.y, 16); - assert.isAbove(dims.x, 100); - assert.isBelow(dims.x, windowWidth); + assert.equal(dims.x, 100); // Move back where it was. - let moverNow = await driver.find('.test-floating-popup-move-handle').getRect(); + const moverNow = await driver.find('.test-floating-popup-move-handle').getRect(); await move({x: moverInitial.x - moverNow.x}); // And restore the size by double clicking the resizer. await driver.withActions((a) => a.doubleClick(driver.find('.test-floating-popup-resize-handle'))); - - // Now test if we can't resize it offscreen. - await move({y: 10000}); - await move({y: -100}); - // Header is about 100px above the viewport. - dims = await driver.find('.test-floating-popup-window').getRect(); - assert.isBelow(dims.y, windowHeight); - assert.isAbove(dims.x, windowHeight - 140); - - // Now resize as far as possible. - await resize({y: 10}); - await resize({y: 300}); - - // Make sure it is still visible. - dims = await driver.find('.test-floating-popup-window').getRect(); - assert.isBelow(dims.y, windowHeight - 16); - - // Now move back and resize. - moverNow = await driver.find('.test-floating-popup-move-handle').getRect(); - await move({x: moverInitial.x - moverNow.x, y: moverInitial.y - moverNow.y}); - await driver.withActions((a) => a.doubleClick(driver.find('.test-floating-popup-resize-handle'))); - - dims = await driver.find('.test-floating-popup-window').getRect(); - assert.equal(dims.height, initialDims.height); - assert.equal(dims.y, initialDims.y); - assert.equal(dims.x, initialDims.x); }); it('is visible on all pages', async function() { for (const page of ['access-rules', 'raw', 'code', 'settings']) { await driver.find(`.test-tools-${page}`).click(); + if (['access-rules', 'code'].includes(page)) { await gu.waitForServer(); } assert.isTrue(await driver.find('.test-doc-tutorial-popup').isDisplayed()); } }); diff --git a/test/nbrowser/FieldSettings.ntest.js b/test/nbrowser/FieldSettings.ntest.js index f5332d17..2294ef93 100644 --- a/test/nbrowser/FieldSettings.ntest.js +++ b/test/nbrowser/FieldSettings.ntest.js @@ -8,6 +8,7 @@ import { $, gu, test } from 'test/nbrowser/gristUtil-nbrowser'; describe('FieldSettings.ntest', function() { const cleanup = test.setupTestSuite(this); + gu.bigScreen(); before(async function() { await gu.supportOldTimeyTestCode(); diff --git a/test/nbrowser/FillLinkedRecords.ntest.js b/test/nbrowser/FillLinkedRecords.ntest.js index bb124994..5bea983a 100644 --- a/test/nbrowser/FillLinkedRecords.ntest.js +++ b/test/nbrowser/FillLinkedRecords.ntest.js @@ -7,6 +7,7 @@ import { $, gu, test } from 'test/nbrowser/gristUtil-nbrowser'; */ describe('FillLinkedRecords.ntest', function() { const cleanup = test.setupTestSuite(this); + const clipboard = gu.getLockableClipboard(); gu.bigScreen(); @@ -130,9 +131,12 @@ describe('FillLinkedRecords.ntest', function() { // Copy a range of three values, and paste them into the Add-New row. await gu.clickCell({col: 2, rowNum: 1}); - await gu.sendKeys([$.SHIFT, $.DOWN, $.DOWN], $.COPY); - await gu.clickCell({col: 2, rowNum: 5}); - await gu.sendKeys($.PASTE); + await gu.sendKeys([$.SHIFT, $.DOWN, $.DOWN]); + await clipboard.lockAndPerform(async (cb) => { + await cb.copy(); + await gu.clickCell({col: 2, rowNum: 5}); + await cb.paste(); + }); await gu.waitForServer(); // Verify that three new rows now show up, with Film auto-filled. diff --git a/test/nbrowser/Pages.ts b/test/nbrowser/Pages.ts index ba1947f8..1e406904 100644 --- a/test/nbrowser/Pages.ts +++ b/test/nbrowser/Pages.ts @@ -8,7 +8,6 @@ import values = require('lodash/values'); describe('Pages', function() { this.timeout(60000); - setupTestSuite(); let doc: DocCreationInfo; let api: UserAPI; let session: Session; diff --git a/test/nbrowser/SavePosition.ntest.js b/test/nbrowser/SavePosition.ntest.js index bce9d9bb..c54b36d6 100644 --- a/test/nbrowser/SavePosition.ntest.js +++ b/test/nbrowser/SavePosition.ntest.js @@ -3,6 +3,7 @@ import { $, gu, test } from 'test/nbrowser/gristUtil-nbrowser'; describe('SavePosition.ntest', function() { const cleanup = test.setupTestSuite(this); + const clipboard = gu.getLockableClipboard(); before(async function() { this.timeout(Math.max(this.timeout(), 20000)); // Long-running test, unfortunately @@ -96,9 +97,11 @@ describe('SavePosition.ntest', function() { it('should paste into saved position', async function() { await gu.getCell({col: 1, rowNum: 9, section: 'Country'}).click(); await gu.actions.selectTabView('City'); - await gu.sendKeys($.COPY); - await gu.actions.selectTabView('Country'); - await gu.sendKeys($.PASTE); + await clipboard.lockAndPerform(async (cb) => { + await cb.copy(); + await gu.actions.selectTabView('Country'); + await cb.paste(); + }); await gu.waitForServer(); assert.deepEqual(await gu.getVisibleGridCells(1, [8, 9, 10]), ['United Arab Emirates', 'Pará', 'Armenia']); diff --git a/test/nbrowser/ViewLayoutCollapse.ts b/test/nbrowser/ViewLayoutCollapse.ts new file mode 100644 index 00000000..ff1ea299 --- /dev/null +++ b/test/nbrowser/ViewLayoutCollapse.ts @@ -0,0 +1,875 @@ +import * as gu from 'test/nbrowser/gristUtils'; +import {setupTestSuite} from 'test/nbrowser/testUtils'; +import {getCollapsedSection, openCollapsedSectionMenu} from 'test/nbrowser/ViewLayoutUtils'; +import {assert, driver, Key, WebElement, WebElementPromise} from 'mocha-webdriver'; +import {arrayRepeat} from 'app/plugin/gutil'; +import {addStatic, serveSomething} from 'test/server/customUtil'; + +const GAP = 16; // Distance between buttons representing collapsed widgets. + +describe("ViewLayoutCollapse", function() { + this.timeout(40000); + const cleanup = setupTestSuite(); + gu.bigScreen(); + let session: gu.Session; + + before(async () => { + session = await gu.session().login(); + await session.tempDoc(cleanup, 'Investment Research.grist'); + await gu.openPage("Overview"); + }); + + it('fix: custom widget should not throw errors when collapsed', async function() { + const revert = await gu.begin(); + + // Add custom section. + await gu.addNewPage('Table', 'Companies'); + await gu.addNewSection('Custom', 'Companies', { selectBy: 'COMPANIES'}); + + // Serve custom widget. + const widgetServer = await serveSomething(app => { + addStatic(app); + }); + cleanup.addAfterAll(widgetServer.shutdown); + await gu.openWidgetPanel(); + await gu.setWidgetUrl(widgetServer.url + '/probe/index.html'); + await driver.find('.test-config-widget-access .test-select-open').click(); + await driver.findContent('.test-select-menu li', 'Full document access').click(); + await gu.waitForServer(); + + // Collapse it. + await collapseByMenu('COMPANIES Custom'); + + // Change cursor position in the active section. + await gu.getCell(2, 4).click(); + + // Put custom section in popup. + await openCollapsedSection('COMPANIES Custom'); + + // Close it by pressing escape. + await gu.sendKeys(Key.ESCAPE); + + // Change cursor once again. + await gu.getCell(2, 5).click(); + + // Make sure we don't have an error (there was a bug here). + await gu.checkForErrors(); + + await revert(); + }); + + it('fix: should resize other sections correctly when maximized and linked', async function() { + const revert = await gu.begin(); + // If there are two sections linked, but one is collapsed, and user is changing the row + // in the popup of the maximized section, the other section should resize correctly. + + // Add two linked tables. + await gu.addNewTable('Table1'); + await gu.addNewSection('Table', 'New Table'); + + await gu.toggleSidePanel('right', 'open'); + + // Set A in Table2 to be linked in Table1, by ref column + await gu.openColumnMenu('A', 'Column Options'); + + // Change it to the Ref column of TABLE1 + await gu.setType('Reference'); + await gu.setRefTable('Table1'); + await gu.setRefShowColumn('A'); + + // Select it by Table1. + await gu.selectBy('TABLE1'); + + // Now add 2 records with 'White' and 'Black' in Table1 + await gu.sendActions([ + ['BulkAddRecord', 'Table1', arrayRepeat(2, null), { A: ['White', 'Black'] }], + // And 30 records in Table2 that are connected to White. + ['BulkAddRecord', 'Table2', arrayRepeat(30, null), { A: arrayRepeat(30, 1) }], + // And 30 records in Table2 that are connected to Black. + ['BulkAddRecord', 'Table2', arrayRepeat(30, null), { A: arrayRepeat(30, 2) }], + ]); + + // Now select White in Table1. + await gu.getCell('A', 1, 'Table1').click(); + + // Now expand Table1. + await gu.expandSection(); + + // Change to black. + await gu.getCell('A', 2, 'Table1').click(); + + // Close popup by sending ESCAPE + await gu.sendKeys(Key.ESCAPE); + + // Make sure we see 30 records in Table2. + const count = await driver.executeScript(` + const section = Array.from(document.querySelectorAll('.test-widget-title-text')) + .find(e => e.textContent === 'TABLE2') + .closest('.viewsection_content'); + return Array.from(section.querySelectorAll('.gridview_data_row_num')).length; + `); + + assert.equal(count, 30 + 1); + + await revert(); + }); + + + it('fix: should support searching', async function() { + // Collapse Companies section (a first one). + await collapseByMenu(COMPANIES); + + // Clear any saved position state. + await driver.executeScript('window.sessionStorage.clear(); window.localStorage.clear();'); + + // Refresh. + await driver.navigate().refresh(); + await gu.waitForDocToLoad(); + + // Here we had a bug that the hidden section was active, and the search was not working as it was + // starting with the hidden section. + + // Now search (for something in the INVESTMENTS section) + await gu.search('2006'); + await gu.closeSearch(); + + // Make sure we don't have an error. + await gu.checkForErrors(); + + assert.equal(await gu.getActiveSectionTitle(), INVESTMENTS); + // Make sure we are in 1column 9th row. + assert.deepEqual(await gu.getCursorPosition(), {rowNum: 9, col: 0}); + + // Hide companies chart, and search for mobile (should show no results). + await collapseByMenu(COMPANIES_CHART); + await gu.search('mobile'); + await gu.hasNoResult(); + await gu.closeSearch(); + + // Open companies in the popup. + await openCollapsedSection(COMPANIES); + // Search for 2006, there will be no results. + await gu.search('2006'); + await gu.hasNoResult(); + // Now search for web. + await gu.closeSearch(); + await gu.search('web'); + assert.deepEqual(await gu.getCursorPosition(), {rowNum: 5, col: 0}); + + // Recreate document (can't undo). + await session.tempDoc(cleanup, 'Investment Research.grist'); + }); + + + it('fix: should not dispose the instance when drag is cancelled', async function() { + const revert = await gu.begin(); + + // Collapse a section. + await collapseByMenu(INVESTMENTS); + + // Drag it and then cancel. + await dragCollapsed(INVESTMENTS); + const logo = driver.find('.test-dm-logo'); + await move(logo, {y: 0}); + await move(logo, {y: -1}); + // Drop it here. + await driver.withActions(actions => actions.release()); + + // Now open it in the full view. + await openCollapsedSection(INVESTMENTS); + + // And make sure we can move cursor. + await gu.getCell(1, 1).click(); + assert.deepEqual(await gu.getCursorPosition(), {rowNum: 1, col: 1}); + await gu.getCell(1, 2).click(); + assert.deepEqual(await gu.getCursorPosition(), {rowNum: 2, col: 1}); + + // Change its type, and check that it works. + await gu.changeWidget('Card List'); + // Undo it. + await gu.undo(); + await gu.getCell(1, 3).click(); + assert.deepEqual(await gu.getCursorPosition(), {rowNum: 3, col: 1}); + + await gu.sendKeys(Key.ESCAPE); + + // Move it back + await dragCollapsed(INVESTMENTS); + + // Move back and drop. + await gu.getSection(COMPANIES_CHART).getRect(); + await move(getDragElement(COMPANIES_CHART)); + await driver.sleep(100); + await move(getDragElement(COMPANIES_CHART), {x : 200}); + await driver.sleep(300); + assert.lengthOf(await driver.findAll(".layout_editor_drop_target.layout_hover"), 1); + + await driver.withActions(actions => actions.release()); + await driver.sleep(600); + + // And make sure we can move cursor. + await gu.getCell(1, 1).click(); + assert.deepEqual(await gu.getCursorPosition(), {rowNum: 1, col: 1}); + await gu.getCell(1, 2).click(); + assert.deepEqual(await gu.getCursorPosition(), {rowNum: 2, col: 1}); + + await waitForSave(); + await revert(); + }); + + + it('fix: should work when the page is refreshed', async function() { + const revert = await gu.begin(); + + await gu.openPage("Companies"); + await gu.selectSectionByTitle("Companies"); + // Go to second row. + await gu.getCell(0, 2).click(); + + // Make sure we see correct company card. + assert.equal(await gu.getCardCell('name', 'COMPANIES Card').getText(), '#NAME?'); + + // Hide first section. + await collapseByMenu("Companies"); + await waitForSave(); + + // Refresh the page. + await driver.navigate().refresh(); + await gu.waitForDocToLoad(); + + // Make sure card is still at the correct row. + await gu.waitToPass(async () => { + assert.equal(await gu.getCardCell('name', 'COMPANIES Card').getText(), '#NAME?'); + }); + + await addToMainByMenu("Companies"); + await revert(); + }); + + it('fix: should support anchor links', async function() { + const revert = await gu.begin(); + + // Open 42Floors in Companies section. + assert.equal(await gu.getActiveSectionTitle(), "COMPANIES"); + await gu.getCell('Link', 11).click(); + assert.equal(await gu.getActiveCell().getText(), '42Floors'); + + // Open 12 row (Alex Bresler, angel). + await gu.getCell('funding_round_type', 12, 'Investments').click(); + assert.equal(await gu.getActiveCell().getText(), 'angel'); + + // Copy anchor link. + const link = await gu.getAnchor(); + + // Collapse first section. + await collapseByMenu("COMPANIES"); + + // Clear any saved position state. + await driver.executeScript('window.sessionStorage.clear(); window.localStorage.clear();'); + + // Navigate to the home screen. + await gu.loadDocMenu('/o/docs'); + + // Now go to the anchor. + await driver.get(link); + await gu.waitForAnchor(); + + const cursor = await gu.getCursorPosition(); + assert.equal(cursor.rowNum, 12); + assert.equal(cursor.col, 1); + assert.equal(await gu.getActiveCell().getText(), 'angel'); + assert.equal(await gu.getActiveSectionTitle(), 'INVESTMENTS'); + assert.match(await driver.getCurrentUrl(), /\/o\/docs\/[^/]+\/Investment-Research\/p\/1$/); + await revert(); + }); + + it("should not autoexpand the tray on a page with a single widget", async () => { + await gu.openPage("Investments"); + assert.equal((await driver.findAll(".viewsection_content")).length, 1); + + // Start drag the main section. + await dragMain("INVESTMENTS"); + + // Move it over the logo, so that the tray thinks that it should expand. + const logo = driver.find('.test-dm-logo'); + await move(logo, {y: 0}); + await move(logo, {y: -1}); + await driver.sleep(100); + + // Make sure the tray was not tricked into expanding itself. + assert.isFalse(await layoutTray().isDisplayed()); + assert.lengthOf(await layoutTray().findAll(".test-layoutTray-empty-box"), 0); // No empty box + + // Drop it on the button, it should go back to where it was. + await driver.withActions(actions => actions.release()); + }); + + it("should autoexpand the tray", async () => { + await gu.openPage("Overview"); + + // Get one of the sections and start dragging it. + await dragMain(COMPANIES_CHART); + + // The tray should not be expanded. + assert.isFalse(await layoutTray().isDisplayed()); + + const logo = driver.find('.test-dm-logo'); + // Now move it to the top, so that tray should be expanded. + await move(logo, {y: 0}); + await driver.sleep(100); + + // Now the tray is visible + assert.isTrue(await layoutTray().isDisplayed()); + assert.lengthOf(await layoutTray().findAll(".test-layoutTray-empty-box"), 1); // One empty box + assert.isTrue(await layoutEditor().matches('[class*=-is-active]')); // Is active + assert.isFalse(await layoutEditor().matches('[class*=-is-target]')); // Is not a target + + // Drop it on the button, it should go back to where it was. + await driver.withActions(actions => actions.release()); + + // The tray should not be expanded. + assert.isFalse(await layoutTray().isDisplayed()); + + await gu.checkForErrors(); + }); + + it("should drag onto main area", async () => { + const revert = await gu.begin(); + await collapseByMenu(COMPANIES); + await collapseByMenu(INVESTMENTS); + + await dragCollapsed(COMPANIES); + const chartCords = await gu.getSection(COMPANIES_CHART).getRect(); + await move(getDragElement(COMPANIES_CHART)); + await driver.sleep(100); + await move(getDragElement(COMPANIES_CHART), {x : 10}); + await driver.sleep(300); + + // We should have a drop target. + const dropTarget = await driver.find(".layout_editor_drop_target.layout_hover"); + const dCords = await dropTarget.getRect(); + // It should be more or less on the left of the chart. + assertDistance(dCords.x, chartCords.x, 20); + assertDistance(dCords.y, chartCords.y, 20); + + // Move away from the drop target. + const addButton = driver.find('.test-dp-add-new'); + await move(addButton); + await driver.sleep(300); + + // Drop target should be gone. + assert.lengthOf(await driver.findAll(".layout_editor_drop_target.layout_hover"), 0); + + // Move back and drop. + await move(getDragElement(COMPANIES_CHART)); + await driver.sleep(100); + // Split the movement into two parts, to make sure layout sees the mouse move. + await move(getDragElement(COMPANIES_CHART), {x : 10}); + await driver.sleep(200); + assert.lengthOf(await driver.findAll(".layout_editor_drop_target.layout_hover"), 1); + await driver.withActions(actions => actions.release()); + await driver.sleep(600); // This animation can be longer. + + // Make sure it was dropped. + assert.lengthOf(await layoutTray().findAll(".test-layoutTray-leaf-box"), 1); // Only one collapsed box. + assert.lengthOf(await layoutTray().findAll(".test-layoutTray-empty-box"), 0); // No empty box. + assert.lengthOf(await layoutTray().findAll(".test-layoutTray-target-box"), 0); // No target box. + assert.deepEqual(await collapsedSectionTitles(), [INVESTMENTS]); // Only investments is collapsed. + assert.deepEqual(await mainSectionTitles(), [COMPANIES, COMPANIES_CHART, INVESTMENTS_CHART]); + // Check that it was dropped on the left top side. + const companiesCords = await gu.getSection(COMPANIES).getRect(); + assertDistance(companiesCords.x, chartCords.x, 20); + assertDistance(companiesCords.y, chartCords.y, 20); + // It should be half as tall as the main layout. + const root = await driver.find(".layout_root").getRect(); + assertDistance(companiesCords.height, root.height / 2, 30); + // And almost half as wide. + assertDistance(companiesCords.width, root.width / 2, 30); + + // Now move it back to the tray. But first collapse another section (so we can test inbetween target). + await collapseByMenu(COMPANIES_CHART); + await dragMain(COMPANIES); + + // Try to move it as the first element. + const firstLeafSize = await firstLeaf().getRect(); + await move(firstLeaf(), { x: -firstLeafSize.width / 2 }); + await driver.sleep(300); + assert.lengthOf(await layoutTray().findAll(".test-layoutTray-target-box"), 1); + // Make sure that the target is in right place. + let target = await layoutTray().find(".test-layoutTray-target-box").getRect(); + assertDistance(target.x, firstLeafSize.x, 10); + + // Now as the second element. + await move(firstLeaf(), { x: firstLeafSize.width / 2 + GAP }); + await driver.sleep(300); + assert.lengthOf(await layoutTray().findAll(".test-layoutTray-target-box"), 1); + target = await layoutTray().find(".test-layoutTray-target-box").getRect(); + assertDistance(target.x, firstLeafSize.x + firstLeafSize.width + GAP, 10); + + // Move away to make sure the target is gone. + await move(addButton); + await driver.sleep(300); + assert.lengthOf(await layoutTray().findAll(".test-layoutTray-target-box"), 0); + + // Move back and drop. + await move(firstLeaf(), { x: firstLeafSize.width / 2 + GAP }); + await driver.sleep(300); + await driver.withActions(actions => actions.release()); + await driver.sleep(600); + + // Make sure it was dropped. + assert.lengthOf(await layoutTray().findAll(".test-layoutTray-leaf-box"), 3); + assert.lengthOf(await layoutTray().findAll(".test-layoutTray-empty-box"), 0); + assert.lengthOf(await layoutTray().findAll(".test-layoutTray-target-box"), 0); + + assert.deepEqual(await collapsedSectionTitles(), [INVESTMENTS, COMPANIES, COMPANIES_CHART]); + assert.deepEqual(await mainSectionTitles(), [INVESTMENTS_CHART]); + + await waitForSave(); // Layout save is debounced 1s. + + // Test couple of undo steps. + await gu.undo(); + assert.deepEqual(await collapsedSectionTitles(), [INVESTMENTS, COMPANIES_CHART]); + assert.deepEqual(await mainSectionTitles(), [COMPANIES, INVESTMENTS_CHART]); + + await gu.undo(); + assert.deepEqual(await collapsedSectionTitles(), [INVESTMENTS]); + assert.deepEqual(await mainSectionTitles(), [COMPANIES, COMPANIES_CHART, INVESTMENTS_CHART]); + + await gu.undo(); + assert.deepEqual(await collapsedSectionTitles(), [COMPANIES, INVESTMENTS]); + assert.deepEqual(await mainSectionTitles(), [COMPANIES_CHART, INVESTMENTS_CHART]); + + await gu.undo(); + assert.deepEqual(await collapsedSectionTitles(), [COMPANIES]); + assert.deepEqual(await mainSectionTitles(), [COMPANIES_CHART, INVESTMENTS_CHART, INVESTMENTS]); + + await gu.redo(); + assert.deepEqual(await collapsedSectionTitles(), [COMPANIES, INVESTMENTS]); + assert.deepEqual(await mainSectionTitles(), [COMPANIES_CHART, INVESTMENTS_CHART]); + + await revert(); + assert.deepEqual(await collapsedSectionTitles(), []); + assert.deepEqual(await mainSectionTitles(), [COMPANIES_CHART, COMPANIES, INVESTMENTS_CHART, INVESTMENTS]); + await gu.checkForErrors(); + }); + + it("should reorder collapsed sections", async () => { + const revert = await gu.begin(); + await collapseByMenu(COMPANIES); + await collapseByMenu(INVESTMENTS); + await collapseByMenu(COMPANIES_CHART); + + await dragCollapsed(COMPANIES); + + // We should see the empty box in the layout. + assert.lengthOf(await layoutTray().findAll(".test-layoutTray-empty-box"), 1); + // The section is actually removed from the layout tray. + assert.lengthOf(await layoutTray().findAll(".test-layoutTray-leaf-box"), 2); + assert.deepEqual(await collapsedSectionTitles(), [INVESTMENTS, COMPANIES_CHART]); + + // Layout should be active and accepting. + assert.isTrue(await layoutEditor().matches('[class*=-is-active]')); + assert.isTrue(await layoutEditor().matches('[class*=-is-target]')); + + // Move mouse somewhere else, layout should not by highlighted. + const addButton = driver.find('.test-dp-add-new'); + await move(addButton); + assert.isTrue(await layoutEditor().matches('[class*=-is-active]')); + assert.isFalse(await layoutEditor().matches('[class*=-is-target]')); + + // Move to the first leaf, and wait for the target to show up. + const first = await firstLeaf().getRect(); + await move(firstLeaf(), {x : -first.width / 2}); + await driver.sleep(300); + assert.lengthOf(await layoutTray().findAll(".test-layoutTray-target-box"), 1); + // Make sure that the target is in right place. + let target = await layoutTray().find(".test-layoutTray-target-box").getRect(); + assert.isBelow(Math.abs(target.x - first.x), 10); + assert.isBelow(Math.abs(target.y - first.y), 10); + assert.isBelow(Math.abs(target.height - first.height), 10); + + // Move away and make sure the target is gone. + await move(addButton); + await driver.sleep(300); + assert.lengthOf(await layoutTray().findAll(".test-layoutTray-target-box"), 0); + + // Move between first and second leaf. + await move(firstLeaf(), {x : first.width / 2 + GAP}); + await driver.sleep(300); + assert.lengthOf(await layoutTray().findAll(".test-layoutTray-target-box"), 1); + target = await layoutTray().find(".test-layoutTray-target-box").getRect(); + assert.isBelow(Math.abs(target.height - first.height), 2); + assert.isBelow(Math.abs(target.y - first.y), 2); + // Should be between first and second leaf. + assert.isBelow(Math.abs(target.x - (first.x + first.width + GAP)), 10); + + // Drop here. + await driver.withActions(actions => actions.release()); + await waitForSave(); // Wait for layout to be saved. + // Target is gone. + assert.lengthOf(await layoutTray().findAll(".test-layoutTray-empty-box"), 0); + // And we have 3 sections in the layout. + assert.lengthOf(await layoutTray().findAll(".test-layoutTray-leaf-box"), 3); + assert.deepEqual(await collapsedSectionTitles(), [INVESTMENTS, COMPANIES, COMPANIES_CHART]); + + // Undo. + await gu.undo(); + // Order should be restored. + assert.deepEqual(await collapsedSectionTitles(), [COMPANIES, INVESTMENTS, COMPANIES_CHART]); + + await revert(); + await gu.checkForErrors(); + }); + + it("should collapse sections and expand using menu", async () => { + await collapseByMenu(COMPANIES_CHART); + await gu.checkForErrors(); + + assert.deepEqual(await collapsedSectionTitles(), [COMPANIES_CHART]); + // Make sure that other sections are not collapsed. + assert.deepEqual(await mainSectionTitles(), [COMPANIES, INVESTMENTS_CHART, INVESTMENTS]); + + await collapseByMenu(INVESTMENTS_CHART); + assert.deepEqual(await collapsedSectionTitles(), [COMPANIES_CHART, INVESTMENTS_CHART]); + assert.deepEqual(await mainSectionTitles(), [COMPANIES, INVESTMENTS]); + + await collapseByMenu(COMPANIES); + assert.deepEqual(await collapsedSectionTitles(), [COMPANIES_CHART, INVESTMENTS_CHART, COMPANIES]); + assert.deepEqual(await mainSectionTitles(), [INVESTMENTS]); + + // The last section is INVESTMENTS, which can't be collapsed. + await gu.openSectionMenu('viewLayout', INVESTMENTS); + assert.equal(await driver.find('.test-section-collapse').matches('[class*=disabled]'), true); + await driver.sendKeys(Key.ESCAPE); + + // Now expand them one by one and test. + await addToMainByMenu(COMPANIES_CHART); + await gu.checkForErrors(); + + assert.deepEqual(await collapsedSectionTitles(), [INVESTMENTS_CHART, COMPANIES]); + assert.deepEqual(await mainSectionTitles(), [INVESTMENTS, COMPANIES_CHART]); + + await addToMainByMenu(INVESTMENTS_CHART); + assert.deepEqual(await collapsedSectionTitles(), [COMPANIES]); + assert.deepEqual(await mainSectionTitles(), [INVESTMENTS, COMPANIES_CHART, INVESTMENTS_CHART]); + await gu.checkForErrors(); + + await addToMainByMenu(COMPANIES); + assert.deepEqual(await collapsedSectionTitles(), []); + assert.deepEqual(await mainSectionTitles(), [INVESTMENTS, COMPANIES_CHART, INVESTMENTS_CHART, COMPANIES]); + await gu.checkForErrors(); + + // Now revert everything using undo but test each step. + await gu.undo(); + assert.deepEqual(await collapsedSectionTitles(), [COMPANIES]); + assert.deepEqual(await mainSectionTitles(), [INVESTMENTS, COMPANIES_CHART, INVESTMENTS_CHART]); + await gu.checkForErrors(); + + await gu.undo(); + assert.deepEqual(await collapsedSectionTitles(), [INVESTMENTS_CHART, COMPANIES]); + assert.deepEqual(await mainSectionTitles(), [INVESTMENTS, COMPANIES_CHART]); + await gu.checkForErrors(); + + await gu.undo(); + assert.deepEqual(await collapsedSectionTitles(), [COMPANIES_CHART, INVESTMENTS_CHART, COMPANIES]); + assert.deepEqual(await mainSectionTitles(), [INVESTMENTS]); + await gu.checkForErrors(); + + await gu.undo(); + assert.deepEqual(await collapsedSectionTitles(), [COMPANIES_CHART, INVESTMENTS_CHART]); + assert.deepEqual(await mainSectionTitles(), [COMPANIES, INVESTMENTS]); + await gu.checkForErrors(); + + await gu.undo(); + assert.deepEqual(await collapsedSectionTitles(), [COMPANIES_CHART]); + assert.deepEqual(await mainSectionTitles(), [COMPANIES, INVESTMENTS_CHART, INVESTMENTS]); + await gu.checkForErrors(); + + await gu.undo(); + assert.deepEqual(await collapsedSectionTitles(), []); + assert.deepEqual(await mainSectionTitles(), [COMPANIES_CHART, COMPANIES, INVESTMENTS_CHART, INVESTMENTS]); + await gu.checkForErrors(); + }); + + it("should remove sections from collapsed tray", async () => { + const revert = await gu.begin(); + // Collapse everything we can. + await collapseByMenu(COMPANIES_CHART); + await collapseByMenu(INVESTMENTS_CHART); + await collapseByMenu(COMPANIES); + assert.deepEqual(await mainSectionTitles(), [INVESTMENTS]); + + // Now remove them using menu. + await removeMiniSection(COMPANIES_CHART); + await gu.checkForErrors(); + + // Check that the section is removed from the collapsed tray. + assert.deepEqual(await collapsedSectionTitles(), [INVESTMENTS_CHART, COMPANIES]); + // Make sure it is stays removed when we move to the other page. + await gu.openPage("Investments"); + // Go back. + await gu.openPage("Overview"); + await gu.checkForErrors(); + + // Test if we see everything as it was. + assert.deepEqual(await collapsedSectionTitles(), [INVESTMENTS_CHART, COMPANIES]); + // Make sure that visible sections are not affected. + assert.deepEqual(await mainSectionTitles(), [INVESTMENTS]); + + // Remove the other sections. + await removeMiniSection(INVESTMENTS_CHART); + await removeMiniSection(COMPANIES); + assert.deepEqual(await collapsedSectionTitles(), []); + assert.deepEqual(await mainSectionTitles(), [INVESTMENTS]); + + // Now revert everything using undo but test each step. + await gu.undo(); + assert.deepEqual(await collapsedSectionTitles(), [COMPANIES]); + assert.deepEqual(await mainSectionTitles(), [INVESTMENTS]); + + await gu.undo(); + assert.deepEqual(await collapsedSectionTitles(), [INVESTMENTS_CHART, COMPANIES]); + assert.deepEqual(await mainSectionTitles(), [INVESTMENTS]); + + await gu.undo(); + assert.deepEqual(await collapsedSectionTitles(), [COMPANIES_CHART, INVESTMENTS_CHART, COMPANIES]); + assert.deepEqual(await mainSectionTitles(), [INVESTMENTS]); + + await gu.undo(); + assert.deepEqual(await collapsedSectionTitles(), [COMPANIES_CHART, INVESTMENTS_CHART]); + assert.deepEqual(await mainSectionTitles(), [COMPANIES, INVESTMENTS]); + + // Ok, we are good, revert back to the original state. + await revert(); + await gu.checkForErrors(); + }); + + it("should switch active section when collapsed", async () => { + const revert = await gu.begin(); + await gu.selectSectionByTitle(gu.exactMatch(COMPANIES)); + // Make sure we are active. + assert.equal(await gu.getActiveSectionTitle(), COMPANIES); + // Collapse it. + await collapseByMenu(COMPANIES); + // Make sure it is collapsed. + assert.deepEqual(await collapsedSectionTitles(), [COMPANIES]); + // Make sure that now COMPANIES_CHART is active. (first one). + assert.equal(await gu.getActiveSectionTitle(), COMPANIES_CHART); + // Expand COMPANIES. + await addToMainByMenu(COMPANIES); + // Make sure that now it is active. + assert.equal(await gu.getActiveSectionTitle(), COMPANIES); + await revert(); + await gu.checkForErrors(); + }); + + it("should show section on popup when clicked", async () => { + const revert = await gu.begin(); + await collapseByMenu(COMPANIES); + await openCollapsedSection(COMPANIES); + // Make sure it is expanded. + assert.isTrue(await driver.find(".test-viewLayout-overlay").matches("[class*=-active]")); + assert.equal(await gu.getActiveSectionTitle(), COMPANIES); + // Make sure that the panel shows it. + await gu.toggleSidePanel('right', 'open'); + await driver.find('.test-config-widget').click(); + assert.equal(await driver.find('.test-right-widget-title').value(), COMPANIES); + // Make sure we see proper items in the menu. + await gu.openSectionMenu('viewLayout', COMPANIES); + // Collapse widget menu item is disabled. + assert.equal(await driver.find('.test-section-collapse').matches('[class*=disabled]'), true); + // Delete widget is enabled. + assert.equal(await driver.find('.test-section-delete').matches('[class*=disabled]'), false); + await driver.sendKeys(Key.ESCAPE); + // Expand button is not visible + assert.lengthOf(await driver.findAll(".active_section .test-section-menu-expandSection"), 0); + // We can rename a section using the popup. + await gu.renameActiveSection("New name"); + assert.equal(await gu.getActiveSectionTitle(), "New name"); + // Make sure the name is reflected in the collapsed tray. + await gu.sendKeys(Key.ESCAPE); + assert.deepEqual(await collapsedSectionTitles(), ["New name"]); + // Open it back. + await openCollapsedSection("New name"); + // Rename it back using undo. + await gu.undo(); + assert.equal(await gu.getActiveSectionTitle(), COMPANIES); + // Now remove it. + await gu.openSectionMenu('viewLayout', COMPANIES); + await driver.find('.test-section-delete').click(); + await gu.waitForServer(); + // Make sure it is closed. + assert.isFalse(await driver.find(".test-viewLayout-overlay").matches("[class*=-active]")); + // Make sure it is removed. + assert.deepEqual(await collapsedSectionTitles(), []); + // Make sure it didn't reappear on the main area. + assert.deepEqual(await mainSectionTitles(), [COMPANIES_CHART, INVESTMENTS_CHART, INVESTMENTS]); + + await revert(); + await gu.checkForErrors(); + }); + + it("should collapse and expand charts without an error", async () => { + const revert = await gu.begin(); + await collapseByMenu(INVESTMENTS); + await dragMain(COMPANIES_CHART); + const firstRect = await firstLeaf().getRect(); + await move(firstLeaf(), { x: firstRect.width / 2 + GAP }); + await driver.sleep(300); + await driver.withActions(actions => actions.release()); + await waitForSave(); // Resize is delayed. + await gu.checkForErrors(); + await revert(); + }); + + it("should drop on the empty space", async () => { + const revert = await gu.begin(); + // Get one of the sections and start dragging it. + await dragMain(COMPANIES_CHART); + // Move it over the logo to show the tray. + const logo = driver.find('.test-dm-logo'); + await move(logo, {y: 0}); + await move(logo, {y: -20}); + await driver.sleep(100); + // Now the tray is visible + assert.isTrue(await layoutTray().isDisplayed()); + // Move it on the empty space just after the empty box + const emptyBox = await layoutTray().find(".test-layoutTray-empty-box"); + const emptyBoxCords = await emptyBox.getRect(); + await move(emptyBox, {x: emptyBoxCords.width + 100 }); + // Make sure that the empty box is not active. + assert.isFalse(await emptyBox.matches('[class*=-is-active]')); + // Drop it here + await driver.withActions(actions => actions.release()); + await driver.sleep(600); // Wait for animation to finish. + await waitForSave(); + // The tray should stay expanded. + assert.isTrue(await layoutTray().isDisplayed()); + + // Check that the section was collapsed. + assert.deepEqual(await collapsedSectionTitles(), [COMPANIES_CHART]); + // And other sections are still there. + assert.deepEqual(await mainSectionTitles(), [COMPANIES, INVESTMENTS_CHART, INVESTMENTS]); + await gu.checkForErrors(); + await revert(); + await gu.checkForErrors(); + }); + + it("should clear layout when dropped section is removed", async () => { + await session.tempNewDoc(cleanup, 'CollapsedBug.grist'); + // Add a new section based on current table. + await gu.addNewSection('Table', 'Table1'); + // It will have id 3 (1 is raw, 2 is visible). + // Collapse it. + await gu.renameActiveSection('ToDelete'); + await collapseByMenu('ToDelete'); + // Remove it from the tray. + await openCollapsedSectionMenu('ToDelete'); + await driver.find('.test-section-delete').click(); + await gu.waitForServer(); + await waitForSave(); + // Now add another one, it will have the same id (3) and it used to be collapsed when added + // as the layout was not cleared. + await gu.addNewSection('Table', 'Table1'); + // Make sure it is expanded. + assert.deepEqual(await mainSectionTitles(), ['TABLE1', 'TABLE1']); + assert.deepEqual(await collapsedSectionTitles(), []); + }); +}); + +async function addToMainByMenu(section: string) { + await openCollapsedSectionMenu(section); + await driver.find('.test-section-expand').click(); + await gu.waitForServer(); + await gu.checkForErrors(); +} + +async function dragCollapsed(section: string) { + const handle = getCollapsedSection(section).find('.draggable-handle'); + await driver.withActions((actions) => actions + .move({origin: handle}) + .press()); + await move(handle, {x : 10, y: 10}); + return handle; +} + +async function dragMain(section: string) { + const handle = gu.getSection(section).find('.viewsection_drag_indicator'); + await driver.withActions((actions) => actions + .move({origin: handle})); + await driver.withActions((actions) => actions + .move({origin: handle, x : 1}) // This is needed to show the drag element. + .press()); + await move(handle, {x : 10, y: 10}); + return handle; +} + +async function openCollapsedSection(section: string) { + await getCollapsedSection(section).find('.draggable-handle').click(); +} + +async function removeMiniSection(section: string) { + await openCollapsedSectionMenu(section); + await driver.find('.test-section-delete').click(); + await gu.waitForServer(); + await gu.checkForErrors(); +} + +async function collapseByMenu(section: string) { + await gu.openSectionMenu('viewLayout', section); + await driver.find('.test-section-collapse').click(); + await gu.waitForServer(); + await gu.checkForErrors(); +} + +// Returns the titles of all collapsed sections. +async function collapsedSectionTitles() { + return await layoutTray().findAll('.test-layoutTray-leaf-box .test-collapsed-section-title', e => e.getText()); +} + +// Returns titles of all sections in the view layout. +async function mainSectionTitles() { + return await driver.findAll('.layout_root .test-viewsection-title', e => e.getText()); +} + +async function move(element: WebElementPromise|WebElement, offset: {x?: number, y?: number} = {x: 0, y: 0}) { + // With current version of webdriver, a fractional values will get ignored, so round to nearest. + if (offset.x) { offset.x = Math.round(offset.x); } + if (offset.y) { offset.y = Math.round(offset.y); } + await driver.withActions(actions => actions.move({origin: element, ...offset})); +} + + +function getDragElement(section: string) { + return gu.getSection(section).find('.viewsection_drag_indicator'); +} + +function layoutTray() { + return driver.find(".test-layoutTray-layout"); +} + +function firstLeaf() { + return layoutTray().find(".test-layoutTray-leaf-box"); +} + +function layoutEditor() { + return driver.find(".test-layoutTray-editor"); +} + +const COMPANIES_CHART = 'COMPANIES [by category_code] Chart'; +const INVESTMENTS_CHART = 'INVESTMENTS [by funded_year] Chart'; +const COMPANIES = 'COMPANIES [by category_code]'; +const INVESTMENTS = 'INVESTMENTS [by funded_year]'; + +function assertDistance(left: number, right: number, max: number) { + return assert.isBelow(Math.abs(left - right), max); +} + +async function waitForSave() { + await gu.waitToPass(async () => { + const pending = await driver.findAll(".test-viewLayout-save-pending"); + assert.isTrue(pending.length === 0); + await gu.waitForServer(); + }, 3000); +} diff --git a/test/nbrowser/ViewLayoutUtils.ts b/test/nbrowser/ViewLayoutUtils.ts new file mode 100644 index 00000000..653a3d47 --- /dev/null +++ b/test/nbrowser/ViewLayoutUtils.ts @@ -0,0 +1,46 @@ +import {assert, driver} from 'mocha-webdriver'; +import * as gu from 'test/nbrowser/gristUtils'; + +export async function closeExpandedSection() { + await driver.find(".test-viewLayout-overlay .test-close-button").click(); + await gu.waitToPass(async () => { + assert.isFalse(await driver.find(".test-viewLayout-overlay").matches("[class*=-active]")); + }); +} + +export async function sectionIsExpanded() { + // Check that we see the overlay + assert.isTrue(await driver.find(".test-viewLayout-overlay").matches("[class*=-active]")); + + // Visually check that the section is expanded. + assert.isTrue(await driver.find(".active_section").isDisplayed()); + const section = await driver.find(".active_section").getRect(); + const doc = await driver.find(".test-gristdoc").getRect(); + assert.isTrue(Math.abs(section.height + 48 - doc.height) < 4); + assert.isTrue(Math.abs(section.width + 112 - doc.width) < 4); + + // Get all other sections on the page and make sure they are hidden. + const otherSections = await driver.findAll(".view_leaf:not(.active_section)"); + for (const otherSection of otherSections) { + assert.isFalse(await otherSection.isDisplayed()); + } + + // Make sure we see the close button. + assert.isTrue(await driver.find(".test-viewLayout-overlay .test-close-button").isDisplayed()); +} + +/** + * Opens the section menu for a collapsed section. + */ +export async function openCollapsedSectionMenu(section: string|RegExp) { + await getCollapsedSection(section).find(`.test-section-menu-viewLayout`).click(); + await driver.findWait('.grist-floating-menu', 100); +} + +export function getCollapsedSection(section: string|RegExp) { + if (typeof section === 'string') { + section = gu.exactMatch(section, 'i'); + } + return driver.findContentWait('.test-collapsed-section .test-collapsed-section-title', section, 100) + .findClosest('.test-collapsed-section'); +} diff --git a/test/nbrowser/WebhookPage.ts b/test/nbrowser/WebhookPage.ts index 639e63fd..940a8139 100644 --- a/test/nbrowser/WebhookPage.ts +++ b/test/nbrowser/WebhookPage.ts @@ -8,6 +8,7 @@ import {EnvironmentSnapshot} from 'test/server/testUtils'; describe('WebhookPage', function () { this.timeout(60000); const cleanup = setupTestSuite(); + const clipboard = gu.getLockableClipboard(); let session: gu.Session; let oldEnv: EnvironmentSnapshot; @@ -192,7 +193,7 @@ describe('WebhookPage', function () { await openWebhookPage(); // Open another tab. - await driver.executeScript("return window.open('about:blank', '_blank')"); + await driver.executeScript("window.open('about:blank', '_blank')"); const [ownerTab, owner2Tab] = await driver.getAllWindowHandles(); await driver.switchTo().window(owner2Tab); @@ -256,9 +257,11 @@ describe('WebhookPage', function () { await openWebhookPage(); await setField(1, 'Name', '1234'); await gu.waitForServer(); - await gu.sendKeys(await gu.copyKey()); - await gu.getDetailCell({col: 'Memo', rowNum: 1}).click(); - await gu.sendKeys(await gu.pasteKey()); + await clipboard.lockAndPerform(async (cb) => { + await cb.copy(); + await gu.getDetailCell({col: 'Memo', rowNum: 1}).click(); + await cb.paste(); + }); await gu.waitForServer(); assert.equal(await getField(1, 'Memo'), '1234'); }); diff --git a/test/nbrowser/chartViewTestUtils.ts b/test/nbrowser/chartViewTestUtils.ts new file mode 100644 index 00000000..135a312b --- /dev/null +++ b/test/nbrowser/chartViewTestUtils.ts @@ -0,0 +1,99 @@ +import {assert, driver, WebElement} from 'mocha-webdriver'; +import {Layout, LayoutAxis, PlotData} from 'plotly.js'; +import * as gu from 'test/nbrowser/gristUtils'; +import isString = require('lodash/isString'); +import isUndefined = require('lodash/isUndefined'); + +export interface ChartData { + data: Partial[]; + layout: Layout; +} + +export async function selectChartType(chartType: string) { + await driver.find('.test-chart-type').click(); + await driver.findContent('.test-select-row', chartType).click(); + return gu.waitForServer(); +} + +export async function getChartData(chartElem?: WebElement|string): Promise { + if (isString(chartElem) || isUndefined(chartElem)) { + const section = isString(chartElem) ? + await gu.getSection(chartElem) : + await driver.findWait('.active_section', 4000); + chartElem = await section.find('.test-chart-container'); + } + return driver.executeScript((el: any) => ({data: el.data, layout: el.layout}), chartElem); +} + +export function checkAxisRange({layout}: ChartData, xMin: number, xMax: number, yMin: number, yMax: number) { + assert.closeTo(layout.xaxis.range![0], xMin, xMin * 0.1); + assert.closeTo(layout.xaxis.range![1], xMax, xMax * 0.1); + assert.closeTo(layout.yaxis.range![0], yMin, yMin * 0.1); + assert.closeTo(layout.yaxis.range![1], yMax, yMax * 0.1); +} + +export function getAxisTitle(axis: Partial): string|undefined { + return axis.title && (axis.title as any).text; +} + +export function findYAxis(name: string) { + return driver.findContent('.test-chart-y-axis', name); +} + +export async function removeYAxis(name: string) { + await findYAxis(name).mouseMove().find('.test-chart-ref-select-remove').click(); + await gu.waitForServer(); +} + +export async function checkAxisConfig(expected: {groupingByColumn?: string|false, + xaxis: string|undefined, yaxis: string[]}) { + const isGroupByPresent = await driver.find('.test-chart-group-by-column').isPresent(); + let groupingByColumn = isGroupByPresent ? await driver.find('.test-chart-group-by-column').getText() : false; + if (groupingByColumn === 'Pick a column') { + groupingByColumn = false; + } + const xaxis = await driver.find('.test-chart-x-axis').getText(); + assert.deepEqual({ + groupingByColumn, + xaxis: xaxis === 'Pick a column' ? undefined : xaxis, + yaxis: await driver.findAll('.test-chart-y-axis', (e) => e.getText()), + }, {...expected, groupingByColumn: expected.groupingByColumn || false}); +} + +export async function setSplitSeries(name: string|false, section?: string) { + await gu.openSectionMenu('viewLayout', section); + await driver.findContent('.grist-floating-menu li', 'Widget options').click(); + + const isChecked = await driver.findContent('label', /Split series/).find('input').matches(':checked'); + if (name === false && isChecked === true || + name && isChecked === false) { + await driver.findContent('label', /Split series/).click(); + } + if (name) { + await driver.find('.test-chart-group-by-column').click(); + await driver.findContent('.test-select-menu li', name || 'Pick a column').click(); + } + await gu.waitForServer(); +} + +export async function selectXAxis(name: string, opt: {noWait?: boolean} = {}) { + await driver.find('.test-chart-x-axis').click(); + await driver.findContent('.test-select-menu li', name).click(); + if (!opt.noWait) { + await gu.waitForServer(); + } +} + + +export async function setYAxis(names: string[]) { + // let's first remove all yaxis and then add new ones + const toRemove = await driver.findAll('.test-chart-y-axis', (e) => e.getText()); + for (const n of toRemove) { await removeYAxis(n); } + for (const n of names) { await addYAxis(n); } +} + +export async function addYAxis(name: string) { + await driver.find('.test-chart-add-y-axis').click(); + await driver.findContent('.grist-floating-menu li', name).click(); + await gu.waitForServer(); +} diff --git a/test/nbrowser/gristUtil-nbrowser.js b/test/nbrowser/gristUtil-nbrowser.js index 72526111..8bb70fff 100644 --- a/test/nbrowser/gristUtil-nbrowser.js +++ b/test/nbrowser/gristUtil-nbrowser.js @@ -17,24 +17,11 @@ function $(key) { return _webdriverjqFactory(key); } -function rep(n, value) { - return _.times(n, () => value); -} - // The "$" object needs some setup done asynchronously. // We do that later, during test initialization. async function applyPatchesToJquerylikeObject($) { $.MOD = await guBase.modKey(); - $.COPY = await guBase.copyKey(); - $.CUT = await guBase.cutKey(); - $.PASTE = await guBase.pasteKey(); $.SELECT_ALL = await guBase.selectAllKey(); - const capabilities = await driver.getCapabilities(); - if (capabilities.getBrowserName() === 'chrome' && await guBase.isMac()) { - $.SELECT_ALL_LINES = (n) => [...rep(n, $.UP), $.HOME, $.SHIFT, ...rep(n, $.DOWN), $.END] - } else { - $.SELECT_ALL_LINES = () => $.SELECT_ALL; - } $.getPage = async (url) => { return driver.get(url); }; @@ -227,8 +214,8 @@ const gu = { return gu.toggleSidePanel('right', 'close'); }, - clickVisibleDetailCells(column, rowNums) { - return gu.getDetailCell(column, rowNums[0]).click(); + clickVisibleDetailCells(column, rowNums, section) { + return gu.getDetailCell(column, rowNums[0], section).click(); }, async clickRowMenuItem(rowNum, item) { @@ -302,9 +289,15 @@ const gu = { * endCell[1]: 0-based column index. **/ async selectGridArea(startCell, endCell) { - let start = await gu.getCell({rowNum: startCell[0], col: startCell[1]}); - let end = await gu.getCell({rowNum: endCell[0], col: endCell[1]}); - await driver.withActions(a => a.click(start).keyDown($.SHIFT).click(end).keyUp($.SHIFT)); + const [startRowNum, startCol] = startCell; + const [endRowNum, endCol] = endCell; + if (startRowNum === endRowNum && startCol === endCol) { + await gu.getCell({rowNum: endRowNum, col: endCol}).click(); + } else { + const start = await gu.getCell({rowNum: startRowNum, col: startCol}); + const end = await gu.getCell({rowNum: endRowNum, col: endCol}); + await driver.withActions(a => a.click(start).keyDown($.SHIFT).click(end).keyUp($.SHIFT)); + } }, /** diff --git a/test/nbrowser/gristUtils.ts b/test/nbrowser/gristUtils.ts index 40c84aaf..c863e6e5 100644 --- a/test/nbrowser/gristUtils.ts +++ b/test/nbrowser/gristUtils.ts @@ -24,6 +24,7 @@ import { Organization as APIOrganization, DocStateComparison, import { Organization } from 'app/gen-server/entity/Organization'; import { Product } from 'app/gen-server/entity/Product'; import { create } from 'app/server/lib/create'; +import { getAppRoot } from 'app/server/lib/places'; import { GristWebDriverUtils, PageWidgetPickerOptions, WindowDimensions as WindowDimensionsBase } from 'test/nbrowser/gristWebDriverUtils'; @@ -33,6 +34,7 @@ import { Cleanup } from 'test/nbrowser/testUtils'; import * as testUtils from 'test/server/testUtils'; import type { AssertionError } from 'assert'; import axios from 'axios'; +import { lock } from 'proper-lockfile'; // tslint:disable:no-namespace // Wrap in a namespace so that we can apply stackWrapOwnMethods to all the exports together. @@ -923,6 +925,10 @@ export async function userActionsVerify(expectedUserActions: unknown[]): Promise if (!Array.isArray(assertError.actual)) { throw new Error('userActionsVerify: no user actions, run userActionsCollect() first'); } + if (!Array.isArray(assertError.expected)) { + throw new Error('userActionsVerify: no expected user actions'); + } + assertError.actual = assertError.actual.map((a: any) => JSON.stringify(a) + ",").join("\n"); assertError.expected = assertError.expected.map((a: any) => JSON.stringify(a) + ",").join("\n"); assert.deepEqual(assertError.actual, assertError.expected); @@ -1402,6 +1408,9 @@ export async function closeSearch() { } export async function closeTooltip() { + if (!await driver.find('.test-tooltip').isPresent()) { return; } + + await driver.find('.test-tooltip').mouseMove(); await driver.mouseMoveBy({x : 100, y: 100}); await waitToPass(async () => { assert.equal(await driver.find('.test-tooltip').isPresent(), false); @@ -1580,29 +1589,16 @@ export async function applyTypeTransform() { } export async function isMac(): Promise { - return /Darwin|Mac|iPod|iPhone|iPad/i.test((await driver.getCapabilities()).get('platform')); + const platform = (await driver.getCapabilities()).getPlatform() ?? ''; + return /Darwin|Mac|mac os x|iPod|iPhone|iPad/i.test(platform); } export async function modKey() { return await isMac() ? Key.COMMAND : Key.CONTROL; } -// For copy-pasting, use different key combinations for Chrome on Mac. -// See http://stackoverflow.com/a/41046276/328565 -export async function copyKey() { - return await isMac() ? Key.chord(Key.CONTROL, Key.INSERT) : Key.chord(Key.CONTROL, 'c'); -} - -export async function cutKey() { - return await isMac() ? Key.chord(Key.CONTROL, Key.DELETE) : Key.chord(Key.CONTROL, 'x'); -} - -export async function pasteKey() { - return await isMac() ? Key.chord(Key.SHIFT, Key.INSERT) : Key.chord(Key.CONTROL, 'v'); -} - export async function selectAllKey() { - return await isMac() ? Key.chord(Key.HOME, Key.SHIFT, Key.END) : Key.chord(Key.CONTROL, 'a'); + return await isMac() ? Key.chord(Key.COMMAND, 'a') : Key.chord(Key.CONTROL, 'a'); } /** @@ -1616,14 +1612,13 @@ export async function sendKeys(...keys: string[]) { const toRelease: string[] = []; for (const part of keys) { for (const key of part) { - if ([Key.SHIFT, Key.CONTROL, Key.ALT, Key.META].includes(key)) { + if ([Key.ALT, Key.CONTROL, Key.SHIFT, Key.COMMAND, Key.META].includes(key)) { a.keyDown(key); toRelease.push(key); } else if (key === Key.NULL) { toRelease.splice(0).reverse().forEach(k => a.keyUp(k)); } else { - a.keyDown(key); - a.keyUp(key); + a.sendKeys(key); } } } @@ -1631,11 +1626,10 @@ export async function sendKeys(...keys: string[]) { } /** - * Clears active input/textarea by sending CTRL HOME + CTRL + SHIFT END + DELETE. + * Clears active input/textarea. */ export async function clearInput() { - const ctrl = await modKey(); - return sendKeys(Key.chord(ctrl, Key.HOME), Key.chord(ctrl, Key.SHIFT, Key.END), Key.DELETE); + return sendKeys(await selectAllKey(), Key.DELETE); } /** @@ -2720,10 +2714,10 @@ export async function getEnabledOptions(): Promise { /** * Runs action in a separate tab, closing the tab after. * In case of an error tab is not closed, consider using cleanupExtraWindows - * on whole test suit if needed. + * on whole test suite if needed. */ export async function onNewTab(action: () => Promise) { - await driver.executeScript("return window.open('about:blank', '_blank')"); + await driver.executeScript("window.open('about:blank', '_blank')"); const tabs = await driver.getAllWindowHandles(); await driver.switchTo().window(tabs[tabs.length - 1]); await action(); @@ -3273,6 +3267,126 @@ export async function changeWidgetAccess(access: 'read table'|'full'|'none') { } } +/* + * Returns an instance of `LockableClipboard`, making sure to unlock it after + * each test. + * + * Recommended for use in contexts where the system clipboard may be accessed by + * multiple parallel processes, such as Mocha tests. + */ +export function getLockableClipboard() { + const cb = new LockableClipboard(); + + afterEach(async () => { + await cb.unlock(); + }); + + return cb; +} + +export interface ILockableClipboard { + lockAndPerform(callback: (clipboard: IClipboard) => Promise): Promise; + unlock(): Promise; +} + +class LockableClipboard implements ILockableClipboard { + private _unlock: (() => Promise) | null = null; + + constructor() { + + } + + public async lockAndPerform(callback: (clipboard: IClipboard) => Promise) { + this._unlock = await lock(path.resolve(getAppRoot(), 'test'), { + lockfilePath: path.join(path.resolve(getAppRoot(), 'test'), '.clipboard.lock'), + retries: { + /* The clipboard generally isn't locked for long, so retry frequently. */ + maxTimeout: 1000, + retries: 20, + }, + }); + try { + await callback(new Clipboard()); + } finally { + await this.unlock(); + } + } + + public async unlock() { + await this._unlock?.(); + this._unlock = null; + } +} + +export type ClipboardAction = 'copy' | 'cut' | 'paste'; + +export interface ClipboardActionOptions { + method?: 'keyboard' | 'menu'; +} + +export interface IClipboard { + copy(options?: ClipboardActionOptions): Promise; + cut(options?: ClipboardActionOptions): Promise; + paste(options?: ClipboardActionOptions): Promise; +} + +class Clipboard implements IClipboard { + constructor() { + + } + + public async copy(options: ClipboardActionOptions = {}) { + await this._performAction('copy', options); + } + + public async cut(options: ClipboardActionOptions = {}) { + await this._performAction('cut', options); + } + + public async paste(options: ClipboardActionOptions = {}) { + await this._performAction('paste', options); + } + + private async _performAction(action: ClipboardAction, options: ClipboardActionOptions) { + const {method = 'keyboard'} = options; + switch (method) { + case 'keyboard': { + await this._performActionWithKeyboard(action); + break; + } + case 'menu': { + await this._performActionWithMenu(action); + break; + } + } + } + + private async _performActionWithKeyboard(action: ClipboardAction) { + switch (action) { + case 'copy': { + await sendKeys(Key.chord(await isMac() ? Key.COMMAND : Key.CONTROL, 'c')); + break; + } + case 'cut': { + await sendKeys(Key.chord(await isMac() ? Key.COMMAND : Key.CONTROL, 'x')); + break; + } + case 'paste': { + await sendKeys(Key.chord(await isMac() ? Key.COMMAND : Key.CONTROL, 'v')); + break; + } + } + } + + private async _performActionWithMenu(action: ClipboardAction) { + const field = await driver.find('.active_section .field_clip.has_cursor'); + await driver.withActions(actions => { actions.contextClick(field); }); + await driver.findWait('.grist-floating-menu', 1000); + const menuItemName = action.charAt(0).toUpperCase() + action.slice(1); + await driver.findContent('.grist-floating-menu li', menuItemName).click(); + } +} + } // end of namespace gristUtils diff --git a/test/server/Comm.ts b/test/server/Comm.ts index 9e81e4a7..e6325a61 100644 --- a/test/server/Comm.ts +++ b/test/server/Comm.ts @@ -278,37 +278,49 @@ describe('Comm', function() { }); async function testMissedResponses(sendShouldFail: boolean) { - const logMessages = await testUtils.captureLog('debug', async () => { - const {cliComm, forwarder} = await startManagedConnection({...assortedMethods, - // An extra method that simulates a lost connection on server side prior to response. - testDisconnect: async function(client, x, y) { - await delay(0); // disconnect on the same tick. - await forwarder.disconnectServerSide(); - if (!sendShouldFail) { - // Add a delay to let the 'close' event get noticed first. - await delay(20); - } - return {x: x, y: y, name: "testDisconnect"}; - }, - }); + let failedSendCount = 0; + + const {cliComm, forwarder} = await startManagedConnection({...assortedMethods, + // An extra method that simulates a lost connection on server side prior to response. + testDisconnect: async function(client, x, y) { + setTimeout(() => forwarder.disconnectServerSide(), 0); + if (!sendShouldFail) { + // Add a delay to let the 'close' event get noticed first. + await delay(20); + } + return {x: x, y: y, name: "testDisconnect"}; + }, + }); + + const resp1 = await cliComm._makeRequest(null, null, "methodSync", "foo", 1); + assert.deepEqual(resp1, {name: 'methodSync', x: "foo", y: 1}); - const resp1 = await cliComm._makeRequest(null, null, "methodSync", "foo", 1); - assert.deepEqual(resp1, {name: 'methodSync', x: "foo", y: 1}); + if (sendShouldFail) { + // In Node 18, the socket is closed during the call to 'testDisconnect'. + // In prior versions of Node, the socket was still disconnecting. + // This test is sensitive to timing and only passes in the latter, unless we + // stub the method below to produce similar behavior in the former. + sandbox.stub(Client.prototype as any, '_sendToWebsocket') + .onFirstCall() + .callsFake(() => { + failedSendCount += 1; + throw new Error('WebSocket is not open'); + }) + .callThrough(); + } - // Make more calls, with a disconnect before they return. The server should queue up responses. - const resp2Promise = cliComm._makeRequest(null, null, "testDisconnect", "foo", 2); - const resp3Promise = cliComm._makeRequest(null, null, "methodAsync", "foo", 3); - assert.equal(await isLongerThan(resp2Promise, 250), true); + // Make more calls, with a disconnect before they return. The server should queue up responses. + const resp2Promise = cliComm._makeRequest(null, null, "testDisconnect", "foo", 2); + const resp3Promise = cliComm._makeRequest(null, null, "methodAsync", "foo", 3); + assert.equal(await isLongerThan(resp2Promise, 250), true); - // Once we reconnect, the response should arrive. - await forwarder.connect(); - assert.deepEqual(await resp2Promise, {name: 'testDisconnect', x: "foo", y: 2}); - assert.deepEqual(await resp3Promise, {name: 'methodAsync', x: "foo", y: 3}); - }); + // Once we reconnect, the response should arrive. + await forwarder.connect(); + assert.deepEqual(await resp2Promise, {name: 'testDisconnect', x: "foo", y: 2}); + assert.deepEqual(await resp3Promise, {name: 'methodAsync', x: "foo", y: 3}); - // Check that we saw the situations we were hoping to test. - assert.equal(logMessages.some(m => /^warn: .*send error.*WebSocket is not open/.test(m)), sendShouldFail, - `Expected to see a failed send:\n${logMessages.join('\n')}`); + // Check that we saw the situation we were hoping to test. + assert.equal(failedSendCount, sendShouldFail ? 1 : 0, 'Expected to see a failed send'); } it("should receive all server messages (small) in order when send doesn't fail", async function() { diff --git a/test/server/customUtil.ts b/test/server/customUtil.ts index 789d7422..c2646573 100644 --- a/test/server/customUtil.ts +++ b/test/server/customUtil.ts @@ -20,7 +20,7 @@ export function addStatic(app: express.Express, rootDir?: string) { res.sendFile(req.params[0], {root: path.resolve(getAppRoot(), "static")})); app.use(express.static(rootDir || path.resolve(fixturesRoot, "sites"), { - setHeaders: (res) => { + setHeaders: (res: express.Response) => { res.set("Access-Control-Allow-Origin", "*"); } })); diff --git a/test/server/lib/DocApi.ts b/test/server/lib/DocApi.ts index 8c7750d2..2a8f2c92 100644 --- a/test/server/lib/DocApi.ts +++ b/test/server/lib/DocApi.ts @@ -17,8 +17,8 @@ import { import {delayAbort} from 'app/server/lib/serverUtils'; import axios, {AxiosRequestConfig, AxiosResponse} from 'axios'; import {delay} from 'bluebird'; -import * as bodyParser from 'body-parser'; import {assert} from 'chai'; +import * as express from 'express'; import FormData from 'form-data'; import * as fse from 'fs-extra'; import * as _ from 'lodash'; @@ -3786,7 +3786,7 @@ function testDocApi() { // TODO test retries on failure and slowness in a new test serving = await serveSomething(app => { - app.use(bodyParser.json()); + app.use(express.json()); app.post('/200', ({body}, res) => { successCalled.emit(body[0].A); res.sendStatus(200); diff --git a/test/server/lib/Telemetry.ts b/test/server/lib/Telemetry.ts index b9644697..08991e34 100644 --- a/test/server/lib/Telemetry.ts +++ b/test/server/lib/Telemetry.ts @@ -8,12 +8,24 @@ import {assert} from 'chai'; import * as sinon from 'sinon'; import {TestServer} from 'test/gen-server/apiUtils'; import {configForUser} from 'test/gen-server/testUtils'; +import * as testUtils from 'test/server/testUtils'; const chimpy = configForUser('Chimpy'); const kiwi = configForUser('Kiwi'); const anon = configForUser('Anonymous'); describe('Telemetry', function() { + let oldEnv: testUtils.EnvironmentSnapshot; + + before(async function() { + oldEnv = new testUtils.EnvironmentSnapshot(); + process.env.TYPEORM_DATABASE = ':memory:'; + }); + + after(function() { + oldEnv.restore(); + }); + const variants: [GristDeploymentType, TelemetryLevel, PrefSource][] = [ ['saas', 'off', 'environment-variable'], ['saas', 'limited', 'environment-variable'], diff --git a/test/server/lib/Webhooks-Proxy.ts b/test/server/lib/Webhooks-Proxy.ts index 7ad895b1..d8c45cc8 100644 --- a/test/server/lib/Webhooks-Proxy.ts +++ b/test/server/lib/Webhooks-Proxy.ts @@ -1,8 +1,8 @@ import {UserAPIImpl} from 'app/common/UserAPI'; import {WebhookSubscription} from 'app/server/lib/DocApi'; import axios from 'axios'; -import * as bodyParser from 'body-parser'; import {assert} from 'chai'; +import * as express from 'express'; import {tmpdir} from 'os'; import * as path from 'path'; import {createClient} from 'redis'; @@ -218,7 +218,7 @@ describe('Webhooks-Proxy', function () { this.timeout(30000); serving = await serveSomething(app => { - app.use(bodyParser.json()); + app.use(express.json()); app.post('/200', ({body}, res) => { successCalled.emit(body[0].A); res.sendStatus(200); diff --git a/test/server/lib/helpers/TestProxyServer.ts b/test/server/lib/helpers/TestProxyServer.ts index c5cd472a..d5d171dc 100644 --- a/test/server/lib/helpers/TestProxyServer.ts +++ b/test/server/lib/helpers/TestProxyServer.ts @@ -1,6 +1,5 @@ import {serveSomething, Serving} from "test/server/customUtil"; -import * as bodyParser from "body-parser"; -import {Request, Response} from "express-serve-static-core"; +import * as express from "express"; import axios from "axios"; export class TestProxyServer { @@ -27,8 +26,8 @@ export class TestProxyServer { private async _prepare(portNumber: number) { this._proxyServing = await serveSomething(app => { - app.use(bodyParser.json()); - app.all('*', async (req: Request, res: Response) => { + app.use(express.json()); + app.all('*', async (req: express.Request, res: express.Response) => { this._proxyCallsCounter += 1; let responseCode; try { diff --git a/test/split-tests.js b/test/split-tests.js index 144aa263..2d355eb2 100644 --- a/test/split-tests.js +++ b/test/split-tests.js @@ -1,6 +1,6 @@ /** * This module handles splitting tests for parallelizing them. This module is imported by any run - * of mocha, due by being listed in test/mocha.opts. + * of mocha, due by being listed in package.json. * * It only does anything if TEST_SPLITS is set, which must have the form "3-of-8". * diff --git a/yarn.lock b/yarn.lock index 3f82821d..cf4d000d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -505,11 +505,6 @@ resolved "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz" integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ== -"@sindresorhus/is@^4.0.0": - version "4.6.0" - resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.6.0.tgz#3c7c9c46e678feefe7a2e5bb609d3dbd665ffb3f" - integrity sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw== - "@sinonjs/commons@^1", "@sinonjs/commons@^1.2.0", "@sinonjs/commons@^1.3.0", "@sinonjs/commons@^1.7.0": version "1.8.3" resolved "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz" @@ -556,13 +551,6 @@ dependencies: defer-to-connect "^1.0.1" -"@szmarczak/http-timer@^4.0.5": - version "4.0.6" - resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-4.0.6.tgz#b4a914bb62e7c272d4e5989fe4440f812ab1d807" - integrity sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w== - dependencies: - defer-to-connect "^2.0.0" - "@tokenizer/token@^0.3.0": version "0.3.0" resolved "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz" @@ -604,16 +592,6 @@ "@types/connect" "*" "@types/node" "*" -"@types/cacheable-request@^6.0.1": - version "6.0.3" - resolved "https://registry.yarnpkg.com/@types/cacheable-request/-/cacheable-request-6.0.3.tgz#a430b3260466ca7b5ca5bfd735693b36e7a9d183" - integrity sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw== - dependencies: - "@types/http-cache-semantics" "*" - "@types/keyv" "^3.1.4" - "@types/node" "*" - "@types/responselike" "^1.0.0" - "@types/chai-as-promised@7.1.0": version "7.1.0" resolved "https://registry.npmjs.org/@types/chai-as-promised/-/chai-as-promised-7.1.0.tgz" @@ -631,11 +609,6 @@ resolved "https://registry.npmjs.org/@types/chai/-/chai-4.1.7.tgz" integrity sha512-2Y8uPt0/jwjhQ6EiluT0XCri1Dbplr0ZxfFXUz+ye13gaqE8u5gL5ppao1JrUYr9cIip5S6MvQzBS7Kke7U9VA== -"@types/color-name@^1.1.1": - version "1.1.1" - resolved "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz" - integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== - "@types/connect@*": version "3.4.33" resolved "https://registry.npmjs.org/@types/connect/-/connect-3.4.33.tgz" @@ -696,22 +669,24 @@ resolved "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz" integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g== -"@types/express-serve-static-core@*": - version "4.17.7" - resolved "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.7.tgz" - integrity sha512-EMgTj/DF9qpgLXyc+Btimg+XoH7A2liE8uKul8qSmMTHCeNYzydDKFdsJskDvw42UsesCnhO63dO0Grbj8J4Dw== +"@types/express-serve-static-core@^4.17.33": + version "4.17.35" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.35.tgz#c95dd4424f0d32e525d23812aa8ab8e4d3906c4f" + integrity sha512-wALWQwrgiB2AWTT91CB62b6Yt0sNHpznUXeZEcnPU3DRdlDIz74x8Qg1UUYKSVFi+va5vKOLYRBI1bRKiLLKIg== dependencies: "@types/node" "*" "@types/qs" "*" "@types/range-parser" "*" + "@types/send" "*" -"@types/express@4.16.0": - version "4.16.0" - resolved "https://registry.npmjs.org/@types/express/-/express-4.16.0.tgz" - integrity sha512-TtPEYumsmSTtTetAPXlJVf3kEqb6wZK0bZojpJQrnD/djV4q1oB6QQ8aKvKqwNPACoe02GNiy5zDzcYivR5Z2w== +"@types/express@4.17.17": + version "4.17.17" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.17.tgz#01d5437f6ef9cfa8668e616e13c2f2ac9a491ae4" + integrity sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q== dependencies: "@types/body-parser" "*" - "@types/express-serve-static-core" "*" + "@types/express-serve-static-core" "^4.17.33" + "@types/qs" "*" "@types/serve-static" "*" "@types/form-data@2.2.1": @@ -728,10 +703,10 @@ dependencies: "@types/node" "*" -"@types/http-cache-semantics@*": - version "4.0.1" - resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz#0ea7b61496902b95890dc4c3a116b60cb8dae812" - integrity sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ== +"@types/http-errors@*": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.1.tgz#20172f9578b225f6c7da63446f56d4ce108d5a65" + integrity sha512-/K3ds8TRAfBvi5vfjuz8y6+GiAYBZ0x4tXv1Av6CWBWn0IlADc+ZX9pMq7oU0fNQPnBwIZl3rmeLp6SBApbxSQ== "@types/http-proxy@1.17.9": version "1.17.9" @@ -792,13 +767,6 @@ dependencies: "@types/node" "*" -"@types/keyv@^3.1.4": - version "3.1.4" - resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.4.tgz#3ccdb1c6751b0c7e52300bcdacd5bcbf8faa75b6" - integrity sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg== - dependencies: - "@types/node" "*" - "@types/lodash@4.14.117": version "4.14.117" resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.117.tgz" @@ -824,6 +792,11 @@ resolved "https://registry.npmjs.org/@types/mime/-/mime-2.0.2.tgz" integrity sha512-4kPlzbljFcsttWEq6aBW0OZe6BDajAmyvr2xknBG92tejQnvdGtT9+kXSZ580DqpxY9qG2xeQVF9Dq0ymUTo5Q== +"@types/mime@^1": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a" + integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw== + "@types/minio@7.0.15": version "7.0.15" resolved "https://registry.yarnpkg.com/@types/minio/-/minio-7.0.15.tgz#6fbf2e17aeae172cbf181ea52b1faa05a601ce42" @@ -831,10 +804,10 @@ dependencies: "@types/node" "*" -"@types/mocha@5.2.5": - version "5.2.5" - resolved "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.5.tgz" - integrity sha512-lAVp+Kj54ui/vLUFxsJTMtWvZraZxum3w3Nwkble2dNuV5VnPA+Mi2oGX9XYJAaIvZi3tn3cbjS/qcJXRb6Bww== +"@types/mocha@10.0.1": + version "10.0.1" + resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-10.0.1.tgz#2f4f65bb08bc368ac39c96da7b2f09140b26851b" + integrity sha512-/fvYntiO1GeICvqbQ3doGDIP97vWmvFt83GKguJ6prmQM2iXZfFcq6YE8KteFyRtX2/h5Hf91BYvPodJKFYv5Q== "@types/moment-timezone@0.5.9": version "0.5.9" @@ -861,10 +834,10 @@ resolved "https://registry.npmjs.org/@types/node/-/node-14.0.1.tgz" integrity sha512-FAYBGwC+W6F9+huFIDtn43cpy7+SzG+atzRiTfdp3inUKL2hXnd4rG8hylJLIh4+hqrQy1P17kvJByE/z825hA== -"@types/node@^14": - version "14.18.21" - resolved "https://registry.npmjs.org/@types/node/-/node-14.18.21.tgz" - integrity sha512-x5W9s+8P4XteaxT/jKF0PSb7XEvo5VmqEWgsMlyeY4ZlLK8I6aH6g5TPPyDlLAep+GYf4kefb7HFyc7PAO3m+Q== +"@types/node@18.11.9": + version "18.11.9" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.9.tgz#02d013de7058cea16d36168ef2fc653464cfbad4" + integrity sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg== "@types/node@^14.0.1": version "14.17.6" @@ -890,6 +863,13 @@ dependencies: "@types/d3" "^3" +"@types/proper-lockfile@4.1.2": + version "4.1.2" + resolved "https://registry.yarnpkg.com/@types/proper-lockfile/-/proper-lockfile-4.1.2.tgz#49537cee7134055ee13a1833b76a1c298f39bb26" + integrity sha512-kd4LMvcnpYkspDcp7rmXKedn8iJSCoa331zRRamUp5oanKt/CefbEGPQP7G89enz7sKD4bvsr8mHSsC8j5WOvA== + dependencies: + "@types/retry" "*" + "@types/qrcode@1.4.2": version "1.4.2" resolved "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.4.2.tgz" @@ -915,30 +895,39 @@ "@types/bluebird" "*" "@types/events" "*" -"@types/responselike@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.0.tgz#251f4fe7d154d2bad125abe1b429b23afd262e29" - integrity sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA== - dependencies: - "@types/node" "*" +"@types/retry@*": + version "0.12.2" + resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.2.tgz#ed279a64fa438bb69f2480eda44937912bb7480a" + integrity sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow== "@types/saml2-js@2.0.1": version "2.0.1" resolved "https://registry.npmjs.org/@types/saml2-js/-/saml2-js-2.0.1.tgz" integrity sha512-Gr7528CgFBXqAoMlxdGjj8s8ihgAIXXvVjoUthjZGHJBz2TDyXMObmDALWnFeudcGilxqha/WpFdWJ+FI6RlWA== -"@types/selenium-webdriver@4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@types/selenium-webdriver/-/selenium-webdriver-4.0.0.tgz" - integrity sha512-x/OwFhZaZBkucIl/SuooCmkdu1V6LBfHcBeBV5rj05Co8EenuL1KJiIRUU/Da2hscoqbXyF3QuWDOZZ4sLVA5Q== +"@types/selenium-webdriver@4.1.15": + version "4.1.15" + resolved "https://registry.yarnpkg.com/@types/selenium-webdriver/-/selenium-webdriver-4.1.15.tgz#d346b674b96f5ba43f1ad5343f997030559e5b23" + integrity sha512-oQ15G3q3EZ0dS049SB/5zx2tQkIS2kmDQWC/TSfAHJYKvXLZoUiLaPXnfSwbLP8Q5lcJeu5oYjKVSEV0t3H6Bg== + dependencies: + "@types/ws" "*" + +"@types/send@*": + version "0.17.1" + resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.1.tgz#ed4932b8a2a805f1fe362a70f4e62d0ac994e301" + integrity sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q== + dependencies: + "@types/mime" "^1" + "@types/node" "*" "@types/serve-static@*": - version "1.13.3" - resolved "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.3.tgz" - integrity sha512-oprSwp094zOglVrXdlo/4bAHtKTAxX6VT8FOZlBKrmyLbNvE1zxZyJ6yikMVtHIvwP45+ZQGJn+FdXGKTozq0g== + version "1.15.2" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.2.tgz#3e5419ecd1e40e7405d34093f10befb43f63381a" + integrity sha512-J2LqtvFYCzaj8pVYKw8klQXrLLk7TBZmQ4ShlcdkELFKGwGMfevMLneMMRkMgZxotOD9wg497LpC7O8PcvAmfw== dependencies: - "@types/express-serve-static-core" "*" + "@types/http-errors" "*" "@types/mime" "*" + "@types/node" "*" "@types/sinon@5.0.5": version "5.0.5" @@ -989,6 +978,13 @@ resolved "https://registry.npmjs.org/@types/which/-/which-2.0.1.tgz" integrity sha512-Jjakcv8Roqtio6w1gr0D7y6twbhx6gGgFGF5BLwajPpnOIOxFkakFhCq+LmyyeAz7BX6ULrjBOxdKaCDy+4+dQ== +"@types/ws@*": + version "8.5.6" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.6.tgz#e9ad51f0ab79b9110c50916c9fcbddc36d373065" + integrity sha512-8B5EO9jLVCy+B58PLHvLDuOD8DRVMgQzq8d55SjLCOn9kqGyqOvy27exVaTio1q1nX5zLu8/6N0n2ThSxOM6tg== + dependencies: + "@types/node" "*" + "@types/ws@^6": version "6.0.4" resolved "https://registry.npmjs.org/@types/ws/-/ws-6.0.4.tgz" @@ -1259,13 +1255,13 @@ accept-language-parser@1.5.0: resolved "https://registry.npmjs.org/accept-language-parser/-/accept-language-parser-1.5.0.tgz" integrity sha1-iHfFQECo3LWeCgfZwf3kIpgzR5E= -accepts@~1.3.5: - version "1.3.7" - resolved "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz" - integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== +accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== dependencies: - mime-types "~2.1.24" - negotiator "0.6.2" + mime-types "~2.1.34" + negotiator "0.6.3" ace-builds@1.23.3: version "1.23.3" @@ -1374,11 +1370,6 @@ acorn@^8.4.1, acorn@^8.5.0: resolved "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz" integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A== -adm-zip@0.5.9: - version "0.5.9" - resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.5.9.tgz#b33691028333821c0cf95c31374c5462f2905a83" - integrity sha512-s+3fXLkeeLjZ2kLjCBwQufpI5fuN+kIGBxu6530nVQZGVol0d7Y/M88/xw9HGGUcJjKf8LutN3VPRUBq6N7Ajg== - agent-base@6, agent-base@^6.0.2: version "6.0.2" resolved "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz" @@ -1435,11 +1426,6 @@ ansi-regex@^4.1.0: resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz" integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== -ansi-regex@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz" - integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== - ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" @@ -1452,21 +1438,13 @@ ansi-styles@^3.2.1: dependencies: color-convert "^1.9.0" -ansi-styles@^4.0.0: +ansi-styles@^4.0.0, ansi-styles@^4.1.0: version "4.3.0" - resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== dependencies: color-convert "^2.0.1" -ansi-styles@^4.1.0: - version "4.2.1" - resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz" - integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA== - dependencies: - "@types/color-name" "^1.1.1" - color-convert "^2.0.1" - any-base@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/any-base/-/any-base-1.1.0.tgz" @@ -1574,7 +1552,7 @@ argparse@^1.0.7: argparse@^2.0.1: version "2.0.1" - resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== array-filter@~0.0.0: @@ -1602,23 +1580,11 @@ array-reduce@~0.0.0: resolved "https://registry.npmjs.org/array-reduce/-/array-reduce-0.0.0.tgz" integrity sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys= -array-union@^1.0.1: - version "1.0.2" - resolved "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz" - integrity sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk= - dependencies: - array-uniq "^1.0.1" - array-union@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== -array-uniq@^1.0.1: - version "1.0.3" - resolved "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz" - integrity sha1-r2rId6Jcx/dOBYiUdThY39sk/bY= - arrify@^2.0.0: version "2.0.1" resolved "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz" @@ -1655,7 +1621,7 @@ assert@^1.4.0: assertion-error@^1.1.0: version "1.1.0" - resolved "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz" + resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== async-mutex@0.2.4: @@ -1722,9 +1688,9 @@ backbone@1.3.3: underscore ">=1.8.3" balanced-match@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz" - integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== base64-js@^1.2.0, base64-js@^1.3.0, base64-js@^1.3.1: version "1.5.1" @@ -1761,9 +1727,9 @@ bignumber.js@^9.0.0: integrity sha512-IdZR9mh6ahOBv/hYGiXyVuyCetmGJhtYkqLBpTStdhEGjegpPlUawydyaF3pbIOFynJTpllEs+NP+CS9jKFLjA== binary-extensions@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz" - integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow== + version "2.2.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" + integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== binary@~0.3.0: version "0.3.0" @@ -1789,7 +1755,7 @@ block-stream2@^2.0.0: dependencies: readable-stream "^3.4.0" -bluebird@3.7.2, bluebird@^3.3.3, bluebird@^3.5.0: +bluebird@^3.3.3, bluebird@^3.5.0: version "3.7.2" resolved "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== @@ -1814,21 +1780,23 @@ bn.js@^5.1.1: resolved "https://registry.npmjs.org/bn.js/-/bn.js-5.1.1.tgz" integrity sha512-IUTD/REb78Z2eodka1QZyyEk66pciRcP6Sroka0aI3tG/iwIdYLrBD62RsubR7vqdt3WyX8p4jxeatzmRSphtA== -body-parser@1.18.3: - version "1.18.3" - resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz" - integrity sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ= +body-parser@1.20.1: + version "1.20.1" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" + integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== dependencies: - bytes "3.0.0" + bytes "3.1.2" content-type "~1.0.4" debug "2.6.9" - depd "~1.1.2" - http-errors "~1.6.3" - iconv-lite "0.4.23" - on-finished "~2.3.0" - qs "6.5.2" - raw-body "2.3.3" - type-is "~1.6.16" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.11.0" + raw-body "2.5.1" + type-is "~1.6.18" + unpipe "1.0.0" bootstrap-datepicker@1.9.0: version "1.9.0" @@ -1863,7 +1831,7 @@ boxen@^4.2.0: brace-expansion@^1.1.7: version "1.1.11" - resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== dependencies: balanced-match "^1.0.0" @@ -1919,7 +1887,7 @@ browser-resolve@^1.11.0, browser-resolve@^1.7.0: browser-stdout@1.3.1: version "1.3.1" - resolved "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== browserify-aes@^1.0.0, browserify-aes@^1.0.4: @@ -2055,10 +2023,10 @@ browserslist@^4.21.3: node-releases "^2.0.6" update-browserslist-db "^1.0.9" -buffer-crc32@^0.2.1, buffer-crc32@^0.2.13, buffer-crc32@~0.2.3: +buffer-crc32@^0.2.1, buffer-crc32@^0.2.13: version "0.2.13" - resolved "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz" - integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= + resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" + integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ== buffer-equal-constant-time@1.0.1: version "1.0.1" @@ -2116,10 +2084,10 @@ builtin-status-codes@^3.0.0: resolved "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz" integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug= -bytes@3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz" - integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg= +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== cacache@^15.2.0: version "15.3.0" @@ -2145,11 +2113,6 @@ cacache@^15.2.0: tar "^6.0.2" unique-filename "^1.1.1" -cacheable-lookup@^5.0.3: - version "5.0.4" - resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz#5a6b865b2c44357be3d5ebc2a467b032719a7005" - integrity sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA== - cacheable-request@^6.0.0: version "6.1.0" resolved "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz" @@ -2163,19 +2126,6 @@ cacheable-request@^6.0.0: normalize-url "^4.1.0" responselike "^1.0.2" -cacheable-request@^7.0.2: - version "7.0.4" - resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-7.0.4.tgz#7a33ebf08613178b403635be7b899d3e69bbe817" - integrity sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg== - dependencies: - clone-response "^1.0.2" - get-stream "^5.1.0" - http-cache-semantics "^4.0.0" - keyv "^4.0.0" - lowercase-keys "^2.0.0" - normalize-url "^6.0.1" - responselike "^2.0.0" - cached-path-relative@^1.0.0: version "1.1.0" resolved "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.1.0.tgz" @@ -2254,14 +2204,15 @@ chai@4.2.0: type-detect "^4.0.5" chai@^4.1.2: - version "4.3.4" - resolved "https://registry.npmjs.org/chai/-/chai-4.3.4.tgz" - integrity sha512-yS5H68VYOCtN1cjfwumDSuzn/9c+yza4f3reKXlE5rUg7SFcCEy90gJvydNgOYtblyf4Zi6jIWRnXOgErta0KA== + version "4.3.8" + resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.8.tgz#40c59718ad6928da6629c70496fe990b2bb5b17c" + integrity sha512-vX4YvVVtxlfSZ2VecZgFUTU5qPCYsobVI2O9FmwEXBhDigYGQA6jRXCycIs1yJnnWbZ6/+a2zNIF5DfVCcJBFQ== dependencies: assertion-error "^1.1.0" check-error "^1.0.2" - deep-eql "^3.0.1" + deep-eql "^4.1.2" get-func-name "^2.0.0" + loupe "^2.3.1" pathval "^1.1.1" type-detect "^4.0.5" @@ -2299,7 +2250,7 @@ chalk@^4.0.0: chalk@^4.1.0: version "4.1.2" - resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== dependencies: ansi-styles "^4.1.0" @@ -2312,8 +2263,8 @@ chance@1.0.16: check-error@^1.0.2: version "1.0.2" - resolved "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz" - integrity sha1-V00xLt2Iu13YkS6Sht1sCu1KrII= + resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" + integrity sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA== chokidar@3.5.3: version "3.5.3" @@ -2357,17 +2308,6 @@ chrome-trace-event@^1.0.2: dependencies: tslib "^1.9.0" -chromedriver@^74.0.0: - version "74.0.0" - resolved "https://registry.npmjs.org/chromedriver/-/chromedriver-74.0.0.tgz" - integrity sha512-xXgsq0l4gVTY9X5vuccOSVj/iEBm3Bf5MIwzSAASIRJagt4BlWw77SxQq1f4JAJ35/9Ys4NLMA/kWFbd7A/gfQ== - dependencies: - del "^3.0.0" - extract-zip "^1.6.7" - mkdirp "^0.5.1" - request "^2.88.0" - tcp-port-used "^1.0.1" - ci-info@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz" @@ -2414,7 +2354,7 @@ cliui@^6.0.0: cliui@^7.0.2: version "7.0.4" - resolved "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== dependencies: string-width "^4.2.0" @@ -2477,7 +2417,7 @@ collect-js-deps@^0.1.1: color-convert@2.0.1, color-convert@^2.0.1: version "2.0.1" - resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== dependencies: color-name "~1.1.4" @@ -2496,7 +2436,7 @@ color-name@1.1.3: color-name@~1.1.4: version "1.1.4" - resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== color-support@^1.1.2, color-support@^1.1.3: @@ -2568,10 +2508,10 @@ compress-commons@^4.1.0: concat-map@0.0.1: version "0.0.1" - resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" - integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== -concat-stream@^1.6.1, concat-stream@^1.6.2: +concat-stream@^1.6.1: version "1.6.2" resolved "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz" integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== @@ -2625,10 +2565,12 @@ constants-browserify@~1.0.0: resolved "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz" integrity sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U= -content-disposition@0.5.2: - version "0.5.2" - resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz" - integrity sha1-DPaLud318r55YcOoUXjLhdunjLQ= +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" content-type@~1.0.4: version "1.0.4" @@ -2673,11 +2615,16 @@ cookie@0.5.0: resolved "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz" integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== -core-util-is@1.0.2, core-util-is@~1.0.0: +core-util-is@1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz" integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + crc-32@^1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/crc-32/-/crc-32-1.2.0.tgz" @@ -2841,14 +2788,14 @@ dayjs@^1.8.34: resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.10.6.tgz" integrity sha512-AztC/IOW4L1Q41A86phW5Thhcrco3xuAA+YX/BLpLWWjRcTj5TOt/QImBLmCKlrF7u7k47arTnOyL6GnbG8Hvw== -debug@2.6.9, debug@^2.2.0, debug@^2.6.0, debug@^2.6.9: +debug@2.6.9, debug@^2.2.0, debug@^2.6.0: version "2.6.9" resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== dependencies: ms "2.0.0" -debug@4, debug@4.3.1: +debug@4: version "4.3.1" resolved "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz" integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== @@ -2896,13 +2843,6 @@ decompress-response@^3.3.0: dependencies: mimic-response "^1.0.0" -decompress-response@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc" - integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ== - dependencies: - mimic-response "^3.1.0" - deep-eql@^3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz" @@ -2910,6 +2850,13 @@ deep-eql@^3.0.1: dependencies: type-detect "^4.0.0" +deep-eql@^4.1.2: + version "4.1.3" + resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-4.1.3.tgz#7c7775513092f7df98d8df9996dd085eb668cc6d" + integrity sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw== + dependencies: + type-detect "^4.0.0" + deep-extend@^0.6.0: version "0.6.0" resolved "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz" @@ -2935,11 +2882,6 @@ defer-to-connect@^1.0.1: resolved "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz" integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ== -defer-to-connect@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.1.tgz#8016bdb4143e4632b77a3449c6236277de520587" - integrity sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg== - define-properties@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.4.tgz#0b14d7bd7fbeb2f3572c3a7eda80ea5d57fb05b1" @@ -2958,18 +2900,6 @@ defined@~0.0.0: resolved "https://registry.npmjs.org/defined/-/defined-0.0.0.tgz" integrity sha1-817qfXBekzuvE7LwOz+D2SFAOz4= -del@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/del/-/del-3.0.0.tgz" - integrity sha1-U+z2mf/LyzljdpGrE7rxYIGXZuU= - dependencies: - globby "^6.1.0" - is-path-cwd "^1.0.0" - is-path-in-cwd "^1.0.0" - p-map "^1.1.1" - pify "^3.0.0" - rimraf "^2.2.8" - delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" @@ -2985,16 +2915,16 @@ denque@^1.5.0: resolved "https://registry.npmjs.org/denque/-/denque-1.5.1.tgz" integrity sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw== +depd@2.0.0, depd@~2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + depd@^1.1.2, depd@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== -depd@~2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz" - integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== - deps-sort@^2.0.0: version "2.0.1" resolved "https://registry.npmjs.org/deps-sort/-/deps-sort-2.0.1.tgz" @@ -3013,10 +2943,10 @@ des.js@^1.0.0: inherits "^2.0.1" minimalistic-assert "^1.0.0" -destroy@~1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz" - integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== detect-libc@^2.0.0: version "2.0.1" @@ -3195,7 +3125,7 @@ emoji-regex@^7.0.1: emoji-regex@^8.0.0: version "8.0.0" - resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== emojis-list@^3.0.0: @@ -3415,7 +3345,7 @@ esbuild@^0.14.39: escalade@^3.1.1: version "3.1.1" - resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== escape-goat@^2.0.0: @@ -3630,39 +3560,40 @@ exit-on-epipe@~1.0.1: resolved "https://registry.npmjs.org/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz" integrity sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw== -express@4.16.4: - version "4.16.4" - resolved "https://registry.npmjs.org/express/-/express-4.16.4.tgz" - integrity sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg== +express@4.18.2: + version "4.18.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" + integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== dependencies: - accepts "~1.3.5" + accepts "~1.3.8" array-flatten "1.1.1" - body-parser "1.18.3" - content-disposition "0.5.2" + body-parser "1.20.1" + content-disposition "0.5.4" content-type "~1.0.4" - cookie "0.3.1" + cookie "0.5.0" cookie-signature "1.0.6" debug "2.6.9" - depd "~1.1.2" + depd "2.0.0" encodeurl "~1.0.2" escape-html "~1.0.3" etag "~1.8.1" - finalhandler "1.1.1" + finalhandler "1.2.0" fresh "0.5.2" + http-errors "2.0.0" merge-descriptors "1.0.1" methods "~1.1.2" - on-finished "~2.3.0" - parseurl "~1.3.2" + on-finished "2.4.1" + parseurl "~1.3.3" path-to-regexp "0.1.7" - proxy-addr "~2.0.4" - qs "6.5.2" - range-parser "~1.2.0" - safe-buffer "5.1.2" - send "0.16.2" - serve-static "1.13.2" - setprototypeof "1.1.0" - statuses "~1.4.0" - type-is "~1.6.16" + proxy-addr "~2.0.7" + qs "6.11.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.18.0" + serve-static "1.15.0" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" utils-merge "1.0.1" vary "~1.1.2" @@ -3671,16 +3602,6 @@ extend@^3.0.0, extend@^3.0.2, extend@~3.0.2: resolved "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== -extract-zip@^1.6.7: - version "1.7.0" - resolved "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz" - integrity sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA== - dependencies: - concat-stream "^1.6.2" - debug "^2.6.9" - mkdirp "^0.5.4" - yauzl "^2.10.0" - extsprintf@1.3.0: version "1.3.0" resolved "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz" @@ -3764,13 +3685,6 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" -fd-slicer@~1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz" - integrity sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4= - dependencies: - pend "~1.2.0" - file-entry-cache@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" @@ -3789,7 +3703,7 @@ file-type@16.5.4: fill-range@^7.0.1: version "7.0.1" - resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== dependencies: to-regex-range "^5.0.1" @@ -3799,17 +3713,17 @@ filter-obj@^1.1.0: resolved "https://registry.yarnpkg.com/filter-obj/-/filter-obj-1.1.0.tgz#9b311112bc6c6127a16e016c6c5d7f19e0805c5b" integrity sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ== -finalhandler@1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz" - integrity sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg== +finalhandler@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" + integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== dependencies: debug "2.6.9" encodeurl "~1.0.2" escape-html "~1.0.3" - on-finished "~2.3.0" - parseurl "~1.3.2" - statuses "~1.4.0" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" unpipe "~1.0.0" find-up@5.0.0: @@ -3894,10 +3808,10 @@ form-data@~2.3.2: combined-stream "^1.0.6" mime-types "^2.1.12" -forwarded@~0.1.2: - version "0.1.2" - resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz" - integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== fresh@0.5.2: version "0.5.2" @@ -3929,7 +3843,7 @@ fs-extra@^4.0.2, fs-extra@^4.0.3: fs-extra@^8.0.1: version "8.1.0" - resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== dependencies: graceful-fs "^4.2.0" @@ -3953,8 +3867,8 @@ fs-mkdirp-stream@^1.0.0: fs.realpath@^1.0.0: version "1.0.0" - resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" - integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== fsevents@~2.1.2: version "2.1.3" @@ -3962,9 +3876,9 @@ fsevents@~2.1.2: integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== fsevents@~2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" - integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== fstream@^1.0.12: version "1.0.12" @@ -4034,17 +3948,6 @@ gcp-metadata@^4.2.0: gaxios "^4.0.0" json-bigint "^1.0.0" -geckodriver@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/geckodriver/-/geckodriver-3.2.0.tgz#6b0a85e2aafbce209bca30e2d53af857707b1034" - integrity sha512-p+qR2RKlI/TQoCEYrSuTaYCLqsJNni96WmEukTyXmOmLn+3FLdgPAEwMZ0sG2Cwi9hozUzGAWyT6zLuhF6cpiQ== - dependencies: - adm-zip "0.5.9" - bluebird "3.7.2" - got "11.8.5" - https-proxy-agent "5.0.1" - tar "6.1.11" - gensync@^1.0.0-beta.2: version "1.0.0-beta.2" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" @@ -4062,8 +3965,8 @@ get-caller-file@^2.0.1, get-caller-file@^2.0.5: get-func-name@^2.0.0: version "2.0.0" - resolved "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz" - integrity sha1-6td0q+5y4gQJQzoGY2YCPdaIekE= + resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41" + integrity sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig== get-intrinsic@^1.0.2, get-intrinsic@^1.1.1: version "1.1.1" @@ -4147,7 +4050,7 @@ glob-to-regexp@^0.4.1: resolved "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== -glob@*, glob@^7.0.3, glob@^7.1.0, glob@^7.1.3, glob@^7.1.4: +glob@*, glob@^7.1.0, glob@^7.1.4: version "7.1.6" resolved "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz" integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== @@ -4171,9 +4074,9 @@ glob@7.2.0: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.1.1, glob@^7.1.6, glob@^7.2.0: +glob@^7.1.1, glob@^7.1.3, glob@^7.1.6, glob@^7.2.0: version "7.2.3" - resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== dependencies: fs.realpath "^1.0.0" @@ -4222,17 +4125,6 @@ globby@^11.1.0: merge2 "^1.4.1" slash "^3.0.0" -globby@^6.1.0: - version "6.1.0" - resolved "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz" - integrity sha1-9abXDoOV4hyFj7BInWTfAkJNUGw= - dependencies: - array-union "^1.0.1" - glob "^7.0.3" - object-assign "^4.0.1" - pify "^2.0.0" - pinkie-promise "^2.0.0" - globwatcher@~1.2.2: version "1.2.3" resolved "https://registry.npmjs.org/globwatcher/-/globwatcher-1.2.3.tgz" @@ -4284,23 +4176,6 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" -got@11.8.5: - version "11.8.5" - resolved "https://registry.yarnpkg.com/got/-/got-11.8.5.tgz#ce77d045136de56e8f024bebb82ea349bc730046" - integrity sha512-o0Je4NvQObAuZPHLFoRSkdG2lTgtcynqymzg2Vupdx6PorhaT5MCbIyXG6d4D94kk8ZG57QeosgdiqfJWhEhlQ== - dependencies: - "@sindresorhus/is" "^4.0.0" - "@szmarczak/http-timer" "^4.0.5" - "@types/cacheable-request" "^6.0.1" - "@types/responselike" "^1.0.0" - cacheable-lookup "^5.0.3" - cacheable-request "^7.0.2" - decompress-response "^6.0.0" - http2-wrapper "^1.0.0-beta.5.2" - lowercase-keys "^2.0.0" - p-cancelable "^2.0.0" - responselike "^2.0.0" - got@^9.6.0: version "9.6.0" resolved "https://registry.npmjs.org/got/-/got-9.6.0.tgz" @@ -4328,7 +4203,12 @@ graceful-fs@^4.1.2: resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz" integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== -graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.2: +graceful-fs@^4.1.6, graceful-fs@^4.2.0: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +graceful-fs@^4.2.2: version "4.2.6" resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz" integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== @@ -4394,7 +4274,7 @@ has-flag@^3.0.0: has-flag@^4.0.0: version "4.0.0" - resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== has-property-descriptors@^1.0.0: @@ -4471,7 +4351,7 @@ hdr-histogram-percentiles-obj@^3.0.0: he@1.2.0: version "1.2.0" - resolved "https://registry.npmjs.org/he/-/he-1.2.0.tgz" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== highlight.js@10.7.3, highlight.js@^10.7.1: @@ -4510,15 +4390,16 @@ http-cache-semantics@^4.1.0: resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a" integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ== -http-errors@1.6.3, http-errors@~1.6.2, http-errors@~1.6.3: - version "1.6.3" - resolved "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz" - integrity sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0= +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== dependencies: - depd "~1.1.2" - inherits "2.0.3" - setprototypeof "1.1.0" - statuses ">= 1.4.0 < 2" + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" http-errors@~1.8.0: version "1.8.0" @@ -4567,14 +4448,6 @@ http-signature@~1.2.0: jsprim "^1.2.2" sshpk "^1.7.0" -http2-wrapper@^1.0.0-beta.5.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-1.0.3.tgz#b8f55e0c1f25d4ebd08b3b0c2c079f9590800b3d" - integrity sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg== - dependencies: - quick-lru "^5.1.1" - resolve-alpn "^1.0.0" - https-browserify@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz" @@ -4582,7 +4455,7 @@ https-browserify@^1.0.0: https-proxy-agent@5.0.1: version "5.0.1" - resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== dependencies: agent-base "6" @@ -4610,10 +4483,10 @@ i18n-iso-countries@6.1.0: dependencies: diacritics "1.3.0" -i18next-http-middleware@3.2.1: - version "3.2.1" - resolved "https://registry.npmjs.org/i18next-http-middleware/-/i18next-http-middleware-3.2.1.tgz" - integrity sha512-zBwXxDChT0YLoTXIR6jRuqnUUhXW0Iw7egoTnNXyaDRtTbfWNXwU0a53ThyuRPQ+k+tXu3ZMNKRzfLuononaRw== +i18next-http-middleware@3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/i18next-http-middleware/-/i18next-http-middleware-3.3.2.tgz#6a24fee6bde44952a5af24364d43fa32f6c1b9b6" + integrity sha512-PSeLXQXr9Qiv9Q3GCWCoIJenKVbxCcVsXb7VMp/mOprV4gu+AMJT7VHw4+QEf6oYW6GU31QSLnfDpLNoSMtx3g== i18next-scanner@4.1.0: version "4.1.0" @@ -4655,13 +4528,6 @@ i18next@21.9.1, i18next@^21.0.1: dependencies: "@babel/runtime" "^7.17.2" -iconv-lite@0.4.23: - version "0.4.23" - resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz" - integrity sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA== - dependencies: - safer-buffer ">= 2.1.2 < 3" - iconv-lite@0.4.24: version "0.4.24" resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz" @@ -4698,8 +4564,8 @@ image-size@0.6.3: immediate@~3.0.5: version "3.0.6" - resolved "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz" - integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps= + resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" + integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ== import-fresh@^3.0.0, import-fresh@^3.2.1: version "3.3.0" @@ -4744,8 +4610,8 @@ infer-owner@^1.0.4: inflight@^1.0.4: version "1.0.6" - resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" - integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== dependencies: once "^1.3.0" wrappy "1" @@ -4798,11 +4664,6 @@ interpret@^2.2.0: resolved "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz" integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw== -ip-regex@^4.1.0: - version "4.3.0" - resolved "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz" - integrity sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q== - ip@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ip/-/ip-2.0.0.tgz#4cf4ab182fee2314c75ede1276f8c80b479936da" @@ -4836,7 +4697,7 @@ is-arguments@^1.0.4: is-binary-path@~2.1.0: version "2.1.0" - resolved "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== dependencies: binary-extensions "^2.0.0" @@ -4874,8 +4735,8 @@ is-core-module@^2.9.0: is-extglob@^2.1.0, is-extglob@^2.1.1: version "2.1.1" - resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" - integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== is-fullwidth-code-point@^2.0.0: version "2.0.0" @@ -4884,7 +4745,7 @@ is-fullwidth-code-point@^2.0.0: is-fullwidth-code-point@^3.0.0: version "3.0.0" - resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== is-generator-function@^1.0.7: @@ -4901,20 +4762,13 @@ is-glob@^3.1.0: dependencies: is-extglob "^2.1.0" -is-glob@^4.0.0, is-glob@^4.0.3: +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: version "4.0.3" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== dependencies: is-extglob "^2.1.1" -is-glob@^4.0.1, is-glob@~4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz" - integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== - dependencies: - is-extglob "^2.1.1" - is-installed-globally@^0.3.1: version "0.3.2" resolved "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.3.2.tgz" @@ -4940,7 +4794,7 @@ is-npm@^4.0.0: is-number@^7.0.0: version "7.0.0" - resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== is-obj@^2.0.0: @@ -4948,25 +4802,6 @@ is-obj@^2.0.0: resolved "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz" integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== -is-path-cwd@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz" - integrity sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0= - -is-path-in-cwd@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz" - integrity sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ== - dependencies: - is-path-inside "^1.0.0" - -is-path-inside@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz" - integrity sha1-jvW33lBDej/cprToZe96pVy0gDY= - dependencies: - path-is-inside "^1.0.1" - is-path-inside@^3.0.1: version "3.0.2" resolved "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.2.tgz" @@ -5029,11 +4864,6 @@ is-unicode-supported@^0.1.0: resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== -is-url@^1.2.4: - version "1.2.4" - resolved "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz" - integrity sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww== - is-utf8@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" @@ -5054,15 +4884,6 @@ is-yarn-global@^0.3.0: resolved "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz" integrity sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw== -is2@^2.0.6: - version "2.0.6" - resolved "https://registry.npmjs.org/is2/-/is2-2.0.6.tgz" - integrity sha512-+Z62OHOjA6k2sUDOKXoZI3EXv7Fb1K52jpTBLbkfx62bcUeSsrTBLhEquCRDKTx0XE5XbHcG/S2vrtE3lnEDsQ== - dependencies: - deep-is "^0.1.3" - ip-regex "^4.1.0" - is-url "^1.2.4" - isarray@0.0.1: version "0.0.1" resolved "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" @@ -5070,8 +4891,8 @@ isarray@0.0.1: isarray@~1.0.0: version "1.0.0" - resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" - integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== isexe@^2.0.0: version "2.0.0" @@ -5186,11 +5007,6 @@ json-buffer@3.0.0: resolved "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz" integrity sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg= -json-buffer@3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" - integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== - json-parse-even-better-errors@^2.3.1: version "2.3.1" resolved "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz" @@ -5247,8 +5063,8 @@ json5@^2.2.1: jsonfile@^4.0.0: version "4.0.0" - resolved "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz" - integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss= + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + integrity sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg== optionalDependencies: graceful-fs "^4.1.6" @@ -5287,7 +5103,7 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" -jszip@^3.5.0: +jszip@^3.10.1, jszip@^3.5.0: version "3.10.1" resolved "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz" integrity sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g== @@ -5343,13 +5159,6 @@ keyv@^3.0.0: dependencies: json-buffer "3.0.0" -keyv@^4.0.0: - version "4.5.2" - resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.2.tgz#0e310ce73bf7851ec702f2eaf46ec4e3805cce56" - integrity sha512-5MHbFaKn8cNSmVW7BYnijeAVlE4cYA/SVkifVgrh7yotnfhKmjuXpDKjrABLnT0SfHWV21P8ow07OGfRrNDg8g== - dependencies: - json-buffer "3.0.1" - kind-of@^6.0.2: version "6.0.3" resolved "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz" @@ -5407,7 +5216,7 @@ levn@~0.3.0: lie@~3.3.0: version "3.3.0" - resolved "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz" + resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a" integrity sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ== dependencies: immediate "~3.0.5" @@ -5594,6 +5403,13 @@ lolex@^5.0.1: dependencies: "@sinonjs/commons" "^1.7.0" +loupe@^2.3.1: + version "2.3.6" + resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.6.tgz#76e4af498103c532d1ecc9be102036a21f787b53" + integrity sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA== + dependencies: + get-func-name "^2.0.0" + lowercase-keys@^1.0.0, lowercase-keys@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz" @@ -5729,7 +5545,7 @@ mime-types@^2.1.12, mime-types@~2.1.19: dependencies: mime-db "1.46.0" -mime-types@^2.1.14, mime-types@^2.1.27: +mime-types@^2.1.14, mime-types@^2.1.27, mime-types@~2.1.34: version "2.1.35" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== @@ -5743,21 +5559,16 @@ mime-types@~2.1.24: dependencies: mime-db "1.44.0" -mime@1.4.1: - version "1.4.1" - resolved "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz" - integrity sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ== +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== mimic-response@^1.0.0, mimic-response@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz" integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== -mimic-response@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" - integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== - minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz" @@ -5768,7 +5579,7 @@ minimalistic-crypto-utils@^1.0.1: resolved "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz" integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= -minimatch@*, minimatch@^3.0.4: +minimatch@*: version "3.0.4" resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz" integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== @@ -5790,7 +5601,7 @@ minimatch@5.0.1: dependencies: brace-expansion "^2.0.1" -minimatch@^3.1.1, minimatch@^3.1.2: +minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== @@ -5896,7 +5707,7 @@ minizlib@^2.0.0, minizlib@^2.1.1: minipass "^3.0.0" yallist "^4.0.0" -"mkdirp@>=0.5 0", mkdirp@^0.5.1, mkdirp@^0.5.4: +"mkdirp@>=0.5 0", mkdirp@^0.5.1: version "0.5.5" resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz" integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== @@ -5908,21 +5719,19 @@ mkdirp@^1.0.3, mkdirp@^1.0.4: resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -mocha-webdriver@0.2.13: - version "0.2.13" - resolved "https://registry.yarnpkg.com/mocha-webdriver/-/mocha-webdriver-0.2.13.tgz#6e39360e08dc32e2de7375c6eaee83bd6aa98cc6" - integrity sha512-xSQ9fTViIucIZs+loRzKhOkUtztMheHYRg2+CrgqBPg9/MYjOWbcUk+uye5r117BN/r57KRuZxvsZRH0Jm0gCg== +mocha-webdriver@0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/mocha-webdriver/-/mocha-webdriver-0.3.1.tgz#5ed238e710ee2e4dfe72208cdd1dc4a15d2aa644" + integrity sha512-4apKuGdB72aEqnT2LdspLCOGXpWui5EJ/mVsw9UcI8ps2SkKHAhHZtHf05CKuTmQJJZcDa2mpcP4oXNXXDsZlA== dependencies: chai "^4.1.2" chai-as-promised "^7.1.1" - chromedriver "^74.0.0" fs-extra "^8.0.1" - geckodriver "^3.2.0" - mocha "^10.1.0" + mocha "^10.2.0" npm-run-path "^3.1.0" - selenium-webdriver "^4.0.0-alpha.1" + selenium-webdriver "^4.11.1" -mocha@10.2.0, mocha@^10.1.0: +mocha@10.2.0, mocha@^10.2.0: version "10.2.0" resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.2.0.tgz#1fd4a7c32ba5ac372e03a17eef435bd00e5c68b8" integrity sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg== @@ -6017,7 +5826,7 @@ ms@2.0.0: ms@2.1.2, ms@^2.1.1: version "2.1.2" - resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== ms@2.1.3, ms@^2.0.0: @@ -6053,12 +5862,7 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== -negotiator@0.6.2: - version "0.6.2" - resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz" - integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== - -negotiator@^0.6.2: +negotiator@0.6.3, negotiator@^0.6.2: version "0.6.3" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== @@ -6201,7 +6005,7 @@ normalize-path@^2.1.1: normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" - resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== normalize-url@^4.1.0: @@ -6209,11 +6013,6 @@ normalize-url@^4.1.0: resolved "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz" integrity sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA== -normalize-url@^6.0.1: - version "6.1.0" - resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a" - integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A== - now-and-later@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/now-and-later/-/now-and-later-2.0.1.tgz#8e579c8685764a7cc02cb680380e94f43ccb1f7c" @@ -6223,7 +6022,7 @@ now-and-later@^2.0.0: npm-run-path@^3.1.0: version "3.1.0" - resolved "https://registry.npmjs.org/npm-run-path/-/npm-run-path-3.1.0.tgz" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-3.1.0.tgz#7f91be317f6a466efed3c9f2980ad8a4ee8b0fa5" integrity sha512-Dbl4A/VfiVGLgQv29URL9xshU8XDY1GeLy+fsaZ1AA8JDSfjvr5P5+pzRbWqRSBxk6/DW7MIh8lTM/PaGnP2kg== dependencies: path-key "^3.0.0" @@ -6283,6 +6082,13 @@ object.assign@^4.0.4: has-symbols "^1.0.3" object-keys "^1.1.1" +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + on-finished@~2.3.0: version "2.3.0" resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz" @@ -6297,8 +6103,8 @@ on-headers@~1.0.1, on-headers@~1.0.2: once@^1.3.0, once@^1.3.1, once@^1.3.2, once@^1.4.0: version "1.4.0" - resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== dependencies: wrappy "1" @@ -6357,11 +6163,6 @@ p-cancelable@^1.0.0: resolved "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz" integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw== -p-cancelable@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.1.1.tgz#aab7fbd416582fa32a3db49859c122487c5ed2cf" - integrity sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg== - p-limit@^2.2.0: version "2.3.0" resolved "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz" @@ -6390,11 +6191,6 @@ p-locate@^5.0.0: dependencies: p-limit "^3.0.2" -p-map@^1.1.1: - version "1.2.0" - resolved "https://registry.npmjs.org/p-map/-/p-map-1.2.0.tgz" - integrity sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA== - p-map@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" @@ -6477,7 +6273,7 @@ parse5@^5.1.1: resolved "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz" integrity sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug== -parseurl@~1.3.2, parseurl@~1.3.3: +parseurl@~1.3.3: version "1.3.3" resolved "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== @@ -6494,18 +6290,13 @@ path-dirname@^1.0.0: path-exists@^4.0.0: version "4.0.0" - resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== path-is-absolute@^1.0.0, path-is-absolute@^1.0.1: version "1.0.1" - resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" - integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= - -path-is-inside@^1.0.1: - version "1.0.2" - resolved "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz" - integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM= + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== path-key@^3.0.0, path-key@^3.1.0: version "3.1.1" @@ -6560,11 +6351,6 @@ peek-readable@^4.1.0: resolved "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz" integrity sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg== -pend@~1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz" - integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA= - performance-now@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz" @@ -6626,38 +6412,11 @@ picocolors@^1.0.0: resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz" integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== -picomatch@^2.0.4, picomatch@^2.2.1: - version "2.2.2" - resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz" - integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== - -picomatch@^2.3.1: +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== -pify@^2.0.0: - version "2.3.0" - resolved "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz" - integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= - -pify@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz" - integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= - -pinkie-promise@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz" - integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o= - dependencies: - pinkie "^2.0.0" - -pinkie@^2.0.0: - version "2.0.4" - resolved "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz" - integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= - piscina@3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/piscina/-/piscina-3.2.0.tgz#f5a1dde0c05567775690cccefe59d9223924d154" @@ -6749,7 +6508,7 @@ printj@~1.1.0: process-nextick-args@^2.0.0, process-nextick-args@~2.0.0: version "2.0.1" - resolved "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== process-nextick-args@~1.0.6: @@ -6775,12 +6534,21 @@ promise-retry@^2.0.1: err-code "^2.0.2" retry "^0.12.0" -proxy-addr@~2.0.4: - version "2.0.6" - resolved "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz" - integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw== +proper-lockfile@4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/proper-lockfile/-/proper-lockfile-4.1.2.tgz#c8b9de2af6b2f1601067f98e01ac66baa223141f" + integrity sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA== + dependencies: + graceful-fs "^4.2.4" + retry "^0.12.0" + signal-exit "^3.0.2" + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== dependencies: - forwarded "~0.1.2" + forwarded "0.2.0" ipaddr.js "1.9.1" psl@^1.1.28: @@ -6872,10 +6640,12 @@ qrcode@1.5.0: pngjs "^5.0.0" yargs "^15.3.1" -qs@6.5.2, qs@~6.5.2: - version "6.5.2" - resolved "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz" - integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== +qs@6.11.0: + version "6.11.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== + dependencies: + side-channel "^1.0.4" qs@^6.7.0: version "6.10.1" @@ -6884,6 +6654,11 @@ qs@^6.7.0: dependencies: side-channel "^1.0.4" +qs@~6.5.2: + version "6.5.2" + resolved "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz" + integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== + query-string@^7.1.1: version "7.1.3" resolved "https://registry.yarnpkg.com/query-string/-/query-string-7.1.3.tgz#a1cf90e994abb113a325804a972d98276fe02328" @@ -6914,11 +6689,6 @@ queue-microtask@^1.2.2: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== -quick-lru@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" - integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== - random-bytes@~1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz" @@ -6944,19 +6714,19 @@ randomfill@^1.0.3: randombytes "^2.0.5" safe-buffer "^5.1.0" -range-parser@~1.2.0: +range-parser@~1.2.1: version "1.2.1" - resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== -raw-body@2.3.3: - version "2.3.3" - resolved "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz" - integrity sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw== +raw-body@2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" + integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== dependencies: - bytes "3.0.0" - http-errors "1.6.3" - iconv-lite "0.4.23" + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" unpipe "1.0.0" rc@^1.2.8: @@ -6985,7 +6755,7 @@ read-only-stream@^2.0.0: string_decoder "^1.1.1" util-deprecate "^1.0.1" -readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6: +readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6: version "2.3.7" resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz" integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== @@ -7010,6 +6780,19 @@ readable-stream@~2.0.0: string_decoder "~0.10.x" util-deprecate "~1.0.1" +readable-stream@~2.3.6: + version "2.3.8" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + readable-web-to-node-stream@^3.0.0: version "3.0.2" resolved "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz" @@ -7170,7 +6953,7 @@ request-promise-native@^1.0.9: stealthy-require "^1.1.1" tough-cookie "^2.3.3" -request@^2.88.0, request@^2.88.2: +request@^2.88.2: version "2.88.2" resolved "https://registry.npmjs.org/request/-/request-2.88.2.tgz" integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== @@ -7198,8 +6981,8 @@ request@^2.88.0, request@^2.88.2: require-directory@^2.1.1: version "2.1.1" - resolved "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz" - integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== require-main-filename@^2.0.0: version "2.0.0" @@ -7211,11 +6994,6 @@ requires-port@^1.0.0: resolved "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz" integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== -resolve-alpn@^1.0.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/resolve-alpn/-/resolve-alpn-1.2.1.tgz#b7adbdac3546aaaec20b45e7d8265927072726f9" - integrity sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g== - resolve-cwd@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz" @@ -7274,13 +7052,6 @@ responselike@^1.0.2: dependencies: lowercase-keys "^1.0.0" -responselike@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/responselike/-/responselike-2.0.1.tgz#9a0bc8fdc252f3fb1cca68b016591059ba1422bc" - integrity sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw== - dependencies: - lowercase-keys "^2.0.0" - retry@^0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" @@ -7291,7 +7062,7 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== -rimraf@2, rimraf@^2.2.8, rimraf@^2.7.1: +rimraf@2: version "2.7.1" resolved "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz" integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== @@ -7322,7 +7093,7 @@ run-parallel@^1.1.9: safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" - resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== safe-buffer@5.2.0: @@ -7375,15 +7146,14 @@ schema-utils@^3.1.0, schema-utils@^3.1.1: ajv "^6.12.5" ajv-keywords "^3.5.2" -selenium-webdriver@^4.0.0-alpha.1: - version "4.0.0-beta.2" - resolved "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.0.0-beta.2.tgz" - integrity sha512-uuNl3T1JjhrXCO4UAAy+iIIgZ/PqgYNiYvy+yfWCY+x2vHH9y7tIdD9a/q1rwbf/5jD/ENwYlVuNj46uIngknA== +selenium-webdriver@^4.11.1: + version "4.13.0" + resolved "https://registry.yarnpkg.com/selenium-webdriver/-/selenium-webdriver-4.13.0.tgz#1e06bab7adedb308e3635131bc75bd32038261d5" + integrity sha512-8JS0h5E0Sq7gNfbGg8LVaQ+Eqek97tvOONn3Jmy+NiWfb12WYpftz4VTC4D2JT4wakdG6VUzGKpA8cFGg0IjkA== dependencies: - jszip "^3.5.0" - rimraf "^2.7.1" + jszip "^3.10.1" tmp "^0.2.1" - ws "^7.3.1" + ws ">=8.13.0" semver-diff@^3.1.1: version "3.1.1" @@ -7409,24 +7179,24 @@ semver@^7.3.5, semver@^7.3.7: dependencies: lru-cache "^6.0.0" -send@0.16.2: - version "0.16.2" - resolved "https://registry.npmjs.org/send/-/send-0.16.2.tgz" - integrity sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw== +send@0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" + integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== dependencies: debug "2.6.9" - depd "~1.1.2" - destroy "~1.0.4" + depd "2.0.0" + destroy "1.2.0" encodeurl "~1.0.2" escape-html "~1.0.3" etag "~1.8.1" fresh "0.5.2" - http-errors "~1.6.2" - mime "1.4.1" - ms "2.0.0" - on-finished "~2.3.0" - range-parser "~1.2.0" - statuses "~1.4.0" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" serialize-javascript@6.0.0, serialize-javascript@^6.0.0: version "6.0.0" @@ -7435,15 +7205,15 @@ serialize-javascript@6.0.0, serialize-javascript@^6.0.0: dependencies: randombytes "^2.1.0" -serve-static@1.13.2: - version "1.13.2" - resolved "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz" - integrity sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw== +serve-static@1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" + integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== dependencies: encodeurl "~1.0.2" escape-html "~1.0.3" - parseurl "~1.3.2" - send "0.16.2" + parseurl "~1.3.3" + send "0.18.0" set-blocking@^2.0.0: version "2.0.0" @@ -7455,11 +7225,6 @@ setimmediate@^1.0.5, setimmediate@~1.0.4: resolved "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz" integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA== -setprototypeof@1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz" - integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== - setprototypeof@1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz" @@ -7689,16 +7454,16 @@ stackback@0.0.2: resolved "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz" integrity sha1-Gsig2Ug4SNFpXkGLbQMaPDzmjjs= -"statuses@>= 1.4.0 < 2", "statuses@>= 1.5.0 < 2": +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +"statuses@>= 1.5.0 < 2": version "1.5.0" resolved "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz" integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= -statuses@~1.4.0: - version "1.4.0" - resolved "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz" - integrity sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew== - stealthy-require@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz" @@ -7761,7 +7526,7 @@ strict-uri-encode@^2.0.0: resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546" integrity sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ== -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.2.3: +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -7779,7 +7544,7 @@ string-width@^3.0.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^5.1.0" -string-width@^4.0.0, string-width@^4.1.0: +string-width@^4.0.0: version "4.2.0" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz" integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== @@ -7788,15 +7553,6 @@ string-width@^4.0.0, string-width@^4.1.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.0" -string-width@^4.2.0: - version "4.2.2" - resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz" - integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.0" - string_decoder@^1.1.1: version "1.3.0" resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" @@ -7818,7 +7574,7 @@ string_decoder@~1.0.0: string_decoder@~1.1.1: version "1.1.1" - resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== dependencies: safe-buffer "~5.1.0" @@ -7830,14 +7586,7 @@ strip-ansi@^5.1.0: dependencies: ansi-regex "^4.1.0" -strip-ansi@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz" - integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== - dependencies: - ansi-regex "^5.0.0" - -strip-ansi@^6.0.1: +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -7889,9 +7638,9 @@ supports-color@^5.3.0, supports-color@^5.5.0: has-flag "^3.0.0" supports-color@^7.1.0: - version "7.1.0" - resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz" - integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g== + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== dependencies: has-flag "^4.0.0" @@ -7928,18 +7677,6 @@ tar-stream@^2.2.0: inherits "^2.0.3" readable-stream "^3.1.1" -tar@6.1.11: - version "6.1.11" - resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621" - integrity sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA== - dependencies: - chownr "^2.0.0" - fs-minipass "^2.0.0" - minipass "^3.0.0" - minizlib "^2.1.1" - mkdirp "^1.0.3" - yallist "^4.0.0" - tar@^6.0.2, tar@^6.1.11, tar@^6.1.2: version "6.1.13" resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.13.tgz#46e22529000f612180601a6fe0680e7da508847b" @@ -7952,14 +7689,6 @@ tar@^6.0.2, tar@^6.1.11, tar@^6.1.2: mkdirp "^1.0.3" yallist "^4.0.0" -tcp-port-used@^1.0.1: - version "1.0.2" - resolved "https://registry.npmjs.org/tcp-port-used/-/tcp-port-used-1.0.2.tgz" - integrity sha512-l7ar8lLUD3XS1V2lfoJlCBaeoaWo/2xfYt81hM7VlvR4RrMVFqfmzfhLVk40hAb368uitje5gPtBRL1m/DGvLA== - dependencies: - debug "4.3.1" - is2 "^2.0.6" - term-size@^2.1.0: version "2.2.0" resolved "https://registry.npmjs.org/term-size/-/term-size-2.2.0.tgz" @@ -8100,7 +7829,7 @@ to-readable-stream@^1.0.0: to-regex-range@^5.0.1: version "5.0.1" - resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== dependencies: is-number "^7.0.0" @@ -8117,6 +7846,11 @@ toidentifier@1.0.0: resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz" integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + token-types@^4.1.1: version "4.2.1" resolved "https://registry.npmjs.org/token-types/-/token-types-4.2.1.tgz" @@ -8242,7 +7976,7 @@ type-check@~0.3.2: type-detect@4.0.8, type-detect@^4.0.0, type-detect@^4.0.5, type-detect@^4.0.8: version "4.0.8" - resolved "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== type-fest@^0.20.2: @@ -8255,9 +7989,9 @@ type-fest@^0.8.1: resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== -type-is@~1.6.16: +type-is@~1.6.18: version "1.6.18" - resolved "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== dependencies: media-typer "0.3.0" @@ -8384,7 +8118,7 @@ unique-string@^2.0.0: universalify@^0.1.0: version "0.1.2" - resolved "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== universalify@^0.2.0: @@ -8844,7 +8578,7 @@ wrap-ansi@^6.2.0: wrap-ansi@^7.0.0: version "7.0.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== dependencies: ansi-styles "^4.0.0" @@ -8853,8 +8587,8 @@ wrap-ansi@^7.0.0: wrappy@1: version "1.0.2" - resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" - integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== write-file-atomic@^3.0.0: version "3.0.3" @@ -8871,10 +8605,10 @@ ws@8.13.0: resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0" integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== -ws@^7.3.1: - version "7.4.4" - resolved "https://registry.npmjs.org/ws/-/ws-7.4.4.tgz" - integrity sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw== +ws@>=8.13.0: + version "8.14.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.14.1.tgz#4b9586b4f70f9e6534c7bb1d3dc0baa8b8cf01e0" + integrity sha512-4OOseMUq8AzRBI/7SLMUwO+FEDnguetSk7KMb1sHwvF2w2Wv5Hoj0nlifx8vtGsftE/jWHojPy8sMMzYLJ2G/A== ws@^7.4.4: version "7.5.9" @@ -8971,9 +8705,9 @@ y18n@^4.0.0: integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ== y18n@^5.0.5: - version "5.0.5" - resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.5.tgz" - integrity sha512-hsRUr4FFrvhhRH12wOdfs38Gy7k2FFzB9qgN9v3aLykRq0dRcdcpz5C9FxdS2NuhOrI/628b/KSTJ3rwHysYSg== + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== yallist@^3.0.2: version "3.1.1" @@ -8999,9 +8733,9 @@ yargs-parser@^18.1.2: decamelize "^1.2.0" yargs-parser@^20.2.2: - version "20.2.7" - resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.7.tgz" - integrity sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw== + version "20.2.9" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" + integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== yargs-parser@^21.0.0: version "21.1.1" @@ -9061,14 +8795,6 @@ yargs@^17.3.1: y18n "^5.0.5" yargs-parser "^21.0.0" -yauzl@^2.10.0: - version "2.10.0" - resolved "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz" - integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk= - dependencies: - buffer-crc32 "~0.2.3" - fd-slicer "~1.1.0" - yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"