From bcbf57d590b13413fd6b88195a7991b73f06e511 Mon Sep 17 00:00:00 2001 From: Paul Fitzpatrick Date: Tue, 27 Jun 2023 02:11:08 -0400 Subject: [PATCH] (core) bump mocha version to allow parallel tests; move more tests to core Summary: This uses a newer version of mocha in grist-core so that tests can be run in parallel. That allows more tests to be moved without slowing things down overall. Tests moved are venerable browser tests; only the ones that "just work" or worked without too much trouble to are moved, in order to keep the diff from growing too large. Will wrestle with more in follow up. Parallelism is at the file level, rather than the individual test. The newer version of mocha isn't needed for grist-saas repo; tests are parallelized in our internal CI by other means. I've chosen to allocate files to workers in a cruder way than our internal CI, based on initial characters rather than an automated process. The automated process would need some reworking to be compatible with mocha running in parallel mode. Test Plan: this diff was tested first on grist-core, then ported to grist-saas so saas repo history will correctly track history of moved files. Reviewers: jarek Reviewed By: jarek Subscribers: jarek Differential Revision: https://phab.getgrist.com/D3927 --- .eslintrc.js | 1 + .github/workflows/main.yml | 17 +- buildtools/webpack.config.js | 7 + package.json | 24 +- static/mocha.css | 1 + static/mocha.js | 1 + static/test.html | 104 ++ static/testWebdriverJQuery.html | 24 + stubs/app/server/server.ts | 3 + test/client-harness/client.js | 30 + test/client/clientUtil.js | 1 - test/client/components/Layout.js | 2 - test/client/components/commands.js | 2 - test/client/lib/Delay.js | 2 - test/client/lib/ObservableMap.js | 2 - test/client/lib/ObservableSet.js | 2 - test/client/lib/dispose.js | 13 +- test/client/lib/dom.js | 2 - test/client/lib/koArray.js | 2 - test/client/lib/koDom.js | 2 - test/client/lib/koDomScrolly.js | 2 - test/client/lib/koForm.js | 2 - test/client/lib/koUtil.js | 2 - test/client/models/modelUtil.js | 2 - test/client/models/rowset.js | 2 - test/client/models/rowuid.js | 2 - test/common/BinaryIndexedTree.js | 2 - test/common/CircularArray.js | 2 - test/common/MemBuffer.js | 2 - test/common/RecentItems.js | 2 - test/common/arraySplice.js | 2 - test/common/gutil.js | 2 - test/common/marshal.js | 2 - test/common/promises.js | 2 - test/common/serializeTiming.js | 2 - test/common/sortTiming.js | 2 - test/common/timeFormat.js | 2 - test/declarations.d.ts | 8 + test/fixtures/plugins/.jshintrc | 6 + .../plugins/browser-GristDocAPI/main.js | 31 + .../plugins/browser-GristDocAPI/manifest.yml | 12 + .../plugins/custom-section/index-bis.html | 28 + .../plugins/custom-section/index.html | 28 + .../plugins/custom-section/main.js | 14 + .../plugins/custom-section/manifest.yml | 12 + .../custom-section/test-subscribe-api.html | 11 + .../custom-section/test-subscribe-api.js | 36 + .../plugins/dummy-importer/index.html | 22 + .../plugins/dummy-importer/main.js | 9 + .../plugins/dummy-importer/manifest.yml | 16 + .../plugins/dummy-importer/node/main.js | 4 + .../plugins/dummy-importer/sandbox/main.py | 11 + .../plugins/dummy-importer/script.js | 44 + .../builtInPlugins/plugins/2/manifest.yml | 3 + .../plugins/experimental-plugin/manifest.yml | 12 + .../experimental-plugin/sandbox/main.py | 1 + .../invalid-contrib-point/manifest.yml | 3 + .../plugins/long-call/manifest.yml | 14 + .../plugins/long-call/sandbox/main.py | 28 + .../plugins/missing-component/manifest.yml | 9 + .../plugins/missing-safePython/manifest.yml | 10 + .../safePython-deactivate-fast/manifest.yml | 14 + .../sandbox/main.py | 28 + .../testing-function-call-plugin/backend.js | 5 + .../testing-function-call-plugin/manifest.yml | 17 + .../sandbox/main.py | 15 + .../plugins/valid-file-parser/manifest.yml | 11 + .../plugins/valid-file-parser/sandbox/main.py | 29 + .../plugins/valid-import-source/manifest.yml | 10 + .../plugins/wrong-json/manifest.json | 1 + .../plugins/wrong-yaml/manifest.yml | 1 + .../plugins/node-GristDocAPI/TestSubscribe.js | 34 + .../plugins/node-GristDocAPI/main.js | 21 + .../plugins/node-GristDocAPI/manifest.yml | 7 + .../plugins/node-fail/main.js | 3 + .../plugins/node-fail/manifest.yml | 11 + .../plugins/node-mini-csv/manifest.yml | 11 + .../plugins/node-mini-csv/nodebox/main.js | 69 ++ .../node_modules/dependency_test/index.js | 3 + .../node_modules/dependency_test/package.json | 11 + .../plugins/node-wrong-message/main.js | 3 + .../plugins/node-wrong-message/manifest.yml | 11 + .../plugins/valid-import-source/manifest.yml | 10 + test/init-mocha-webdriver.js | 21 +- test/nbrowser/ClientUnitTests.ntest.js | 24 + test/nbrowser/CodeEditor.ntest.js | 61 + test/nbrowser/ColumnOps.ntest.js | 276 +++++ test/nbrowser/CustomWidgetsConfig.ts | 2 +- test/nbrowser/Dates.ntest.js | 504 +++++++++ test/nbrowser/DetailView.ntest.js | 118 ++ test/nbrowser/DocTutorial.ts | 4 +- test/nbrowser/Export.ntest.js | 61 + test/nbrowser/FieldConfigTab.ntest.js | 195 ++++ test/nbrowser/FieldSettings.ntest.js | 324 ++++++ test/nbrowser/FillLinkedRecords.ntest.js | 148 +++ test/nbrowser/GridOptions.ntest.js | 126 +++ test/nbrowser/Health.ntest.js | 17 + test/nbrowser/Localization.ts | 2 +- test/nbrowser/NewDocument.ntest.js | 161 +++ test/nbrowser/Pages.ts | 2 +- test/nbrowser/Properties.ntest.js | 151 +++ test/nbrowser/ReferenceList.ts | 2 +- test/nbrowser/SavePosition.ntest.js | 106 ++ test/nbrowser/SortDates.ntest.js | 141 +++ test/nbrowser/SortEditSave.ntest.js | 63 ++ test/nbrowser/Summaries.ntest.js | 282 +++++ test/nbrowser/TextEditor.ntest.js | 280 +++++ test/nbrowser/TypeChange.ntest.js | 463 ++++++++ test/nbrowser/UndoJumps.ntest.js | 160 +++ test/nbrowser/Validations.ntest.js | 101 ++ test/nbrowser/ViewConfigTab.ntest.js | 71 ++ test/nbrowser/Views.ntest.js | 143 +++ test/nbrowser/WebhookPage.ts | 2 +- test/nbrowser/gristUtil-nbrowser.js | 679 +++++++++++ test/nbrowser/gristUtils.ts | 17 +- test/nbrowser/homeUtil.ts | 12 +- test/nbrowser/testServer.ts | 33 +- test/nbrowser/webdriverjq-nbrowser.js | 554 +++++++++ test/nbrowser/webdriverjq.ntest.js | 215 ++++ test/report-why-tests-hang.js | 21 +- test/server/lib/DocApi.ts | 10 +- test/setupPaths.js | 18 +- test/split-tests.js | 44 +- test/utils.js | 2 +- test/xunit-file.js | 2 +- yarn.lock | 1001 +++++++---------- 126 files changed, 6833 insertions(+), 759 deletions(-) create mode 120000 static/mocha.css create mode 120000 static/mocha.js create mode 100644 static/test.html create mode 100644 static/testWebdriverJQuery.html create mode 100644 test/client-harness/client.js create mode 100644 test/declarations.d.ts create mode 100644 test/fixtures/plugins/.jshintrc create mode 100644 test/fixtures/plugins/browserInstalledPlugins/plugins/browser-GristDocAPI/main.js create mode 100644 test/fixtures/plugins/browserInstalledPlugins/plugins/browser-GristDocAPI/manifest.yml create mode 100644 test/fixtures/plugins/browserInstalledPlugins/plugins/custom-section/index-bis.html create mode 100644 test/fixtures/plugins/browserInstalledPlugins/plugins/custom-section/index.html create mode 100644 test/fixtures/plugins/browserInstalledPlugins/plugins/custom-section/main.js create mode 100644 test/fixtures/plugins/browserInstalledPlugins/plugins/custom-section/manifest.yml create mode 100644 test/fixtures/plugins/browserInstalledPlugins/plugins/custom-section/test-subscribe-api.html create mode 100644 test/fixtures/plugins/browserInstalledPlugins/plugins/custom-section/test-subscribe-api.js create mode 100644 test/fixtures/plugins/browserInstalledPlugins/plugins/dummy-importer/index.html create mode 100644 test/fixtures/plugins/browserInstalledPlugins/plugins/dummy-importer/main.js create mode 100644 test/fixtures/plugins/browserInstalledPlugins/plugins/dummy-importer/manifest.yml create mode 100644 test/fixtures/plugins/browserInstalledPlugins/plugins/dummy-importer/node/main.js create mode 100644 test/fixtures/plugins/browserInstalledPlugins/plugins/dummy-importer/sandbox/main.py create mode 100644 test/fixtures/plugins/browserInstalledPlugins/plugins/dummy-importer/script.js create mode 100644 test/fixtures/plugins/builtInPlugins/plugins/2/manifest.yml create mode 100644 test/fixtures/plugins/builtInPlugins/plugins/experimental-plugin/manifest.yml create mode 100644 test/fixtures/plugins/builtInPlugins/plugins/experimental-plugin/sandbox/main.py create mode 100644 test/fixtures/plugins/builtInPlugins/plugins/invalid-contrib-point/manifest.yml create mode 100644 test/fixtures/plugins/builtInPlugins/plugins/long-call/manifest.yml create mode 100644 test/fixtures/plugins/builtInPlugins/plugins/long-call/sandbox/main.py create mode 100644 test/fixtures/plugins/builtInPlugins/plugins/missing-component/manifest.yml create mode 100644 test/fixtures/plugins/builtInPlugins/plugins/missing-safePython/manifest.yml create mode 100644 test/fixtures/plugins/builtInPlugins/plugins/safePython-deactivate-fast/manifest.yml create mode 100644 test/fixtures/plugins/builtInPlugins/plugins/safePython-deactivate-fast/sandbox/main.py create mode 100644 test/fixtures/plugins/builtInPlugins/plugins/testing-function-call-plugin/backend.js create mode 100644 test/fixtures/plugins/builtInPlugins/plugins/testing-function-call-plugin/manifest.yml create mode 100644 test/fixtures/plugins/builtInPlugins/plugins/testing-function-call-plugin/sandbox/main.py create mode 100644 test/fixtures/plugins/builtInPlugins/plugins/valid-file-parser/manifest.yml create mode 100644 test/fixtures/plugins/builtInPlugins/plugins/valid-file-parser/sandbox/main.py create mode 100644 test/fixtures/plugins/builtInPlugins/plugins/valid-import-source/manifest.yml create mode 100644 test/fixtures/plugins/builtInPlugins/plugins/wrong-json/manifest.json create mode 100644 test/fixtures/plugins/builtInPlugins/plugins/wrong-yaml/manifest.yml create mode 100644 test/fixtures/plugins/installedPlugins/plugins/node-GristDocAPI/TestSubscribe.js create mode 100644 test/fixtures/plugins/installedPlugins/plugins/node-GristDocAPI/main.js create mode 100644 test/fixtures/plugins/installedPlugins/plugins/node-GristDocAPI/manifest.yml create mode 100644 test/fixtures/plugins/installedPlugins/plugins/node-fail/main.js create mode 100644 test/fixtures/plugins/installedPlugins/plugins/node-fail/manifest.yml create mode 100644 test/fixtures/plugins/installedPlugins/plugins/node-mini-csv/manifest.yml create mode 100644 test/fixtures/plugins/installedPlugins/plugins/node-mini-csv/nodebox/main.js create mode 100644 test/fixtures/plugins/installedPlugins/plugins/node-mini-csv/nodebox/node_modules/dependency_test/index.js create mode 100644 test/fixtures/plugins/installedPlugins/plugins/node-mini-csv/nodebox/node_modules/dependency_test/package.json create mode 100644 test/fixtures/plugins/installedPlugins/plugins/node-wrong-message/main.js create mode 100644 test/fixtures/plugins/installedPlugins/plugins/node-wrong-message/manifest.yml create mode 100644 test/fixtures/plugins/installedPlugins/plugins/valid-import-source/manifest.yml create mode 100644 test/nbrowser/ClientUnitTests.ntest.js create mode 100644 test/nbrowser/CodeEditor.ntest.js create mode 100644 test/nbrowser/ColumnOps.ntest.js create mode 100644 test/nbrowser/Dates.ntest.js create mode 100644 test/nbrowser/DetailView.ntest.js create mode 100644 test/nbrowser/Export.ntest.js create mode 100644 test/nbrowser/FieldConfigTab.ntest.js create mode 100644 test/nbrowser/FieldSettings.ntest.js create mode 100644 test/nbrowser/FillLinkedRecords.ntest.js create mode 100644 test/nbrowser/GridOptions.ntest.js create mode 100644 test/nbrowser/Health.ntest.js create mode 100644 test/nbrowser/NewDocument.ntest.js create mode 100644 test/nbrowser/Properties.ntest.js create mode 100644 test/nbrowser/SavePosition.ntest.js create mode 100644 test/nbrowser/SortDates.ntest.js create mode 100644 test/nbrowser/SortEditSave.ntest.js create mode 100644 test/nbrowser/Summaries.ntest.js create mode 100644 test/nbrowser/TextEditor.ntest.js create mode 100644 test/nbrowser/TypeChange.ntest.js create mode 100644 test/nbrowser/UndoJumps.ntest.js create mode 100644 test/nbrowser/Validations.ntest.js create mode 100644 test/nbrowser/ViewConfigTab.ntest.js create mode 100644 test/nbrowser/Views.ntest.js create mode 100644 test/nbrowser/gristUtil-nbrowser.js create mode 100644 test/nbrowser/webdriverjq-nbrowser.js create mode 100644 test/nbrowser/webdriverjq.ntest.js diff --git a/.eslintrc.js b/.eslintrc.js index ddc62c7c..4bb4b1e3 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -4,6 +4,7 @@ module.exports = { env: { node: true, es6: true, + mocha: true, }, // Set parser to support, e.g. import() function for dynamic imports (see // https://stackoverflow.com/a/47833471/328565 and https://stackoverflow.com/a/69557309/328565). diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0ad6ad10..2c3df5e7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -13,6 +13,9 @@ jobs: build_and_test: runs-on: ubuntu-latest strategy: + # it is helpful to know which sets of tests would have succeeded, + # even when there is a failure. + fail-fast: false matrix: python-version: [3.9] node-version: [14.x] @@ -20,11 +23,11 @@ jobs: - ':lint:python:client:common:smoke:' - ':server-1-of-2:' - ':server-2-of-2:' - - ':nbrowser-1-of-5:' - - ':nbrowser-2-of-5:' - - ':nbrowser-3-of-5:' - - ':nbrowser-4-of-5:' - - ':nbrowser-5-of-5:' + - ':nbrowser-^[A-G]:' + - ':nbrowser-^[H-L]:' + - ':nbrowser-^[M-O]:' + - ':nbrowser-^[P-S]:' + - ':nbrowser-^[^A-S]:' steps: - uses: actions/checkout@v3 @@ -95,8 +98,8 @@ jobs: - name: Run main tests without minio and redis if: contains(matrix.tests, ':nbrowser-') run: | - export TEST_SPLITS=$(echo $TESTS | sed "s/.*:nbrowser-\([^:]*\).*/\1/") - MOCHA_WEBDRIVER_HEADLESS=1 yarn run test:nbrowser + export GREP_TESTS=$(echo $TESTS | sed "s/.*:nbrowser-\([^:]*\).*/\1/") + MOCHA_WEBDRIVER_SKIP_CLEANUP=1 MOCHA_WEBDRIVER_HEADLESS=1 yarn run test:nbrowser --parallel --jobs 3 env: TESTS: ${{ matrix.tests }} diff --git a/buildtools/webpack.config.js b/buildtools/webpack.config.js index 26c2832e..9e35f6a7 100644 --- a/buildtools/webpack.config.js +++ b/buildtools/webpack.config.js @@ -15,6 +15,7 @@ module.exports = { account: "app/client/accountMain", billing: "app/client/billingMain", activation: "app/client/activationMain", + test: "test/client-harness/client", }, output: { filename: "[name].bundle.js", @@ -78,4 +79,10 @@ module.exports = { // To strip all locales except “en” new MomentLocalesPlugin() ], + externals: { + // for test bundle: jsdom should not be touched within browser + jsdom: 'alert', + // for test bundle: jquery will be available as jQuery + jquery: 'jQuery' + }, }; diff --git a/package.json b/package.json index 8e920b57..071256ea 100644 --- a/package.json +++ b/package.json @@ -12,12 +12,12 @@ "install:python3": "buildtools/prepare_python3.sh", "build:prod": "buildtools/build.sh", "start:prod": "sandbox/run.sh", - "test": "GRIST_SESSION_COOKIE=grist_test_cookie GRIST_TEST_LOGIN=1 TEST_SUPPORT_API_KEY=api_key_for_support TEST_CLEAN_DATABASE=true NODE_PATH=_build:_build/stubs:_build/ext mocha ${DEBUG:+-b --no-exit} --slow 8000 ${DEBUG:---forbid-only} -g ${GREP_TESTS:-''} '_build/test/common/*.js' '_build/test/client/*.js' '_build/test/nbrowser/*.js' '_build/test/server/**/*.js' '_build/test/gen-server/**/*.js'", - "test:nbrowser": "TEST_SUITE=nbrowser TEST_SUITE_FOR_TIMINGS=nbrowser TIMINGS_FILE=test/timings/nbrowser.txt GRIST_SESSION_COOKIE=grist_test_cookie GRIST_TEST_LOGIN=1 TEST_SUPPORT_API_KEY=api_key_for_support TEST_CLEAN_DATABASE=true NODE_PATH=_build:_build/stubs:_build/ext mocha ${DEBUG:+-b --no-exit} ${DEBUG:---forbid-only} -g ${GREP_TESTS:-''} --slow 8000 -R test/xunit-file '_build/test/nbrowser/**/*.js'", - "test:client": "GRIST_SESSION_COOKIE=grist_test_cookie NODE_PATH=_build:_build/stubs:_build/ext mocha ${DEBUG:+'-b'} '_build/test/client/**/*.js'", - "test:common": "GRIST_SESSION_COOKIE=grist_test_cookie NODE_PATH=_build:_build/stubs:_build/ext mocha ${DEBUG:+'-b'} '_build/test/common/**/*.js'", - "test:server": "TEST_SUITE=server TEST_SUITE_FOR_TIMINGS=server TIMINGS_FILE=test/timings/server.txt GRIST_SESSION_COOKIE=grist_test_cookie NODE_PATH=_build:_build/stubs:_build/ext mocha ${DEBUG:+'-b'} -R test/xunit-file '_build/test/server/**/*.js' '_build/test/gen-server/**/*.js'", - "test:smoke": "NODE_PATH=_build:_build/stubs:_build/ext mocha _build/test/nbrowser/Smoke.js", + "test": "GRIST_SESSION_COOKIE=grist_test_cookie GRIST_TEST_LOGIN=1 TEST_SUPPORT_API_KEY=api_key_for_support TEST_CLEAN_DATABASE=true mocha ${DEBUG:+-b --no-exit} --slow 8000 ${DEBUG:---forbid-only} -g \"${GREP_TESTS}\" '_build/test/common/*.js' '_build/test/client/*.js' '_build/test/nbrowser/*.js' '_build/test/server/**/*.js' '_build/test/gen-server/**/*.js'", + "test:nbrowser": "TEST_SUITE=nbrowser TEST_SUITE_FOR_TIMINGS=nbrowser TIMINGS_FILE=test/timings/nbrowser.txt GRIST_SESSION_COOKIE=grist_test_cookie GRIST_TEST_LOGIN=1 TEST_SUPPORT_API_KEY=api_key_for_support TEST_CLEAN_DATABASE=true mocha ${DEBUG:+-b --no-exit} ${DEBUG:---forbid-only} -g \"${GREP_TESTS}\" --slow 8000 -R test/xunit-file '_build/test/nbrowser/**/*.js'", + "test:client": "GRIST_SESSION_COOKIE=grist_test_cookie mocha ${DEBUG:+'-b'} '_build/test/client/**/*.js'", + "test:common": "GRIST_SESSION_COOKIE=grist_test_cookie mocha ${DEBUG:+'-b'} '_build/test/common/**/*.js'", + "test:server": "TEST_SUITE=server TEST_SUITE_FOR_TIMINGS=server TIMINGS_FILE=test/timings/server.txt GRIST_SESSION_COOKIE=grist_test_cookie mocha ${DEBUG:+'-b'} -R test/xunit-file '_build/test/server/**/*.js' '_build/test/gen-server/**/*.js'", + "test:smoke": "mocha _build/test/nbrowser/Smoke.js", "test:docker": "./test/test_under_docker.sh", "test:python": "sandbox_venv3/bin/python sandbox/grist/runtests.py ${GREP_TESTS:+discover -p \"test*${GREP_TESTS}*.py\"}", "cli": "NODE_PATH=_build:_build/stubs:_build/ext node _build/app/server/companion.js", @@ -91,8 +91,8 @@ "http-proxy": "1.18.1", "i18next-scanner": "4.1.0", "jsdom": "16.5.0", - "mocha": "5.2.0", - "mocha-webdriver": "0.2.9", + "mocha": "10.2.0", + "mocha-webdriver": "0.2.13", "moment-locales-webpack-plugin": "^1.2.0", "nodemon": "^2.0.4", "otplib": "12.0.1", @@ -187,5 +187,13 @@ "jquery": "3.5.0", "ts-interface-checker": "1.0.2", "@gristlabs/sqlite3": "5.1.4-grist.8" + }, + "mocha": { + "require": ["test/setupPaths", + "source-map-support/register", + "test/report-why-tests-hang", + "test/init-mocha-webdriver", + "test/split-tests", + "test/chai-as-promised"] } } diff --git a/static/mocha.css b/static/mocha.css new file mode 120000 index 00000000..50e61022 --- /dev/null +++ b/static/mocha.css @@ -0,0 +1 @@ +../node_modules/mocha/mocha.css \ No newline at end of file diff --git a/static/mocha.js b/static/mocha.js new file mode 120000 index 00000000..39d77e78 --- /dev/null +++ b/static/mocha.js @@ -0,0 +1 @@ +../node_modules/mocha/mocha.js \ No newline at end of file diff --git a/static/test.html b/static/test.html new file mode 100644 index 00000000..c2bf553b --- /dev/null +++ b/static/test.html @@ -0,0 +1,104 @@ + + + + + + + Grist Tests + + + + + + + + + + + + + +
+ +
+
 
+
TBD - RUNNING...
+ + + + diff --git a/static/testWebdriverJQuery.html b/static/testWebdriverJQuery.html new file mode 100644 index 00000000..f1c67f35 --- /dev/null +++ b/static/testWebdriverJQuery.html @@ -0,0 +1,24 @@ + + + + + + WebdriverJQuery test + + + + +
+ + Hello world + +
+
+ + Good bye + + +
+ + + diff --git a/stubs/app/server/server.ts b/stubs/app/server/server.ts index 0aa3776f..dc462275 100644 --- a/stubs/app/server/server.ts +++ b/stubs/app/server/server.ts @@ -110,6 +110,9 @@ export async function main() { if (process.env.GRIST_TESTING_SOCKET) { await server.addTestingHooks(); } + if (process.env.GRIST_SERVE_PLUGINS_PORT) { + await server.startCopy('pluginServer', parseInt(process.env.GRIST_SERVE_PLUGINS_PORT, 10)); + } return server; } diff --git a/test/client-harness/client.js b/test/client-harness/client.js new file mode 100644 index 00000000..6f6dbe1e --- /dev/null +++ b/test/client-harness/client.js @@ -0,0 +1,30 @@ +/* global window */ +window.loadTests = function() { + require('test/common/BinaryIndexedTree'); + require('test/common/CircularArray'); + require('test/common/MemBuffer'); + require('test/common/arraySplice'); + require('test/common/gutil'); + require('test/common/marshal'); + require('test/common/promises'); + require('test/common/serializeTiming'); + require('test/common/timeFormat'); + require('test/common/ValueFormatter'); + require('test/common/InactivityTimer'); + + require('test/client/clientUtil'); + require('test/client/components/Layout'); + require('test/client/components/commands'); + require('test/client/components/sampleLayout'); + require('test/client/lib/ObservableMap'); + require('test/client/lib/ObservableSet'); + require('test/client/lib/dispose'); + require('test/client/lib/dom'); + require('test/client/lib/koArray'); + require('test/client/lib/koDom'); + require('test/client/lib/koForm'); + require('test/client/lib/koUtil'); + require('test/client/models/modelUtil'); + require('test/client/models/rowset'); + require('test/client/lib/localStorageObs'); +} diff --git a/test/client/clientUtil.js b/test/client/clientUtil.js index 3bcba972..e922689c 100644 --- a/test/client/clientUtil.js +++ b/test/client/clientUtil.js @@ -16,7 +16,6 @@ function setTmpMochaGlobals() { return; } - /* global before, after */ const {JSDOM} = require('jsdom'); var prevGlobals; diff --git a/test/client/components/Layout.js b/test/client/components/Layout.js index 1eeaa65f..baf56124 100644 --- a/test/client/components/Layout.js +++ b/test/client/components/Layout.js @@ -1,5 +1,3 @@ -/* global describe, beforeEach, afterEach, it */ - var assert = require('chai').assert; var clientUtil = require('../clientUtil'); var dom = require('app/client/lib/dom'); diff --git a/test/client/components/commands.js b/test/client/components/commands.js index 20f80324..4afb426a 100644 --- a/test/client/components/commands.js +++ b/test/client/components/commands.js @@ -1,5 +1,3 @@ -/* global describe, beforeEach, before, after, it */ - var _ = require('underscore'); var sinon = require('sinon'); var assert = require('chai').assert; diff --git a/test/client/lib/Delay.js b/test/client/lib/Delay.js index f98a9487..57291825 100644 --- a/test/client/lib/Delay.js +++ b/test/client/lib/Delay.js @@ -1,5 +1,3 @@ -/* global describe, it */ - var assert = require('chai').assert; var sinon = require('sinon'); var Promise = require('bluebird'); diff --git a/test/client/lib/ObservableMap.js b/test/client/lib/ObservableMap.js index 7601c184..707924b5 100644 --- a/test/client/lib/ObservableMap.js +++ b/test/client/lib/ObservableMap.js @@ -1,5 +1,3 @@ -/* global describe, it, before */ - const assert = require('chai').assert; const ko = require('knockout'); diff --git a/test/client/lib/ObservableSet.js b/test/client/lib/ObservableSet.js index d1a78dd9..4c1f36d9 100644 --- a/test/client/lib/ObservableSet.js +++ b/test/client/lib/ObservableSet.js @@ -1,5 +1,3 @@ -/* global describe, it */ - var assert = require('chai').assert; var ko = require('knockout'); diff --git a/test/client/lib/dispose.js b/test/client/lib/dispose.js index 0bee1eb4..d60e24ad 100644 --- a/test/client/lib/dispose.js +++ b/test/client/lib/dispose.js @@ -1,5 +1,3 @@ -/* global describe, it, before, after */ - var dispose = require('app/client/lib/dispose'); var bluebird = require('bluebird'); @@ -9,6 +7,8 @@ var sinon = require('sinon'); var clientUtil = require('../clientUtil'); var dom = require('app/client/lib/dom'); +require('chai').config.truncateThreshold = 10000; + describe('dispose', function() { clientUtil.setTmpMochaGlobals(); @@ -153,9 +153,12 @@ describe('dispose', function() { assert.equal(baz.dispose.callCount, 1); assert(baz.dispose.calledBefore(bar.dispose)); - assert.deepEqual(consoleErrors[0], ['Error constructing %s:', 'Foo', 'Error: test-error1']); - assert.deepEqual(consoleErrors[1], ['Error constructing %s:', 'Foo', 'Error: test-error2']); - assert.deepEqual(consoleErrors[2], ['Error constructing %s:', 'Foo', 'Error: test-error3']); + const name = consoleErrors[0][1]; // may be Foo, or minified. + assert(name === 'Foo' || name === 'o'); // this may not be reliable, + // just what I happen to see. + assert.deepEqual(consoleErrors[0], ['Error constructing %s:', name, 'Error: test-error1']); + assert.deepEqual(consoleErrors[1], ['Error constructing %s:', name, 'Error: test-error2']); + assert.deepEqual(consoleErrors[2], ['Error constructing %s:', name, 'Error: test-error3']); assert.equal(consoleErrors.length, 3); }); diff --git a/test/client/lib/dom.js b/test/client/lib/dom.js index 3c753954..5334cb0f 100644 --- a/test/client/lib/dom.js +++ b/test/client/lib/dom.js @@ -1,5 +1,3 @@ -/* global describe, it, before, after */ - var assert = require('chai').assert; var sinon = require('sinon'); var Promise = require('bluebird'); diff --git a/test/client/lib/koArray.js b/test/client/lib/koArray.js index f88ef69c..77b7d82e 100644 --- a/test/client/lib/koArray.js +++ b/test/client/lib/koArray.js @@ -1,5 +1,3 @@ -/* global describe, it */ - var _ = require('underscore'); var assert = require('assert'); var ko = require('knockout'); diff --git a/test/client/lib/koDom.js b/test/client/lib/koDom.js index f2bf9a16..548eb7e5 100644 --- a/test/client/lib/koDom.js +++ b/test/client/lib/koDom.js @@ -1,5 +1,3 @@ -/* global describe, it */ - var assert = require('assert'); var ko = require('knockout'); var sinon = require('sinon'); diff --git a/test/client/lib/koDomScrolly.js b/test/client/lib/koDomScrolly.js index fa419a11..1a8d5bbb 100644 --- a/test/client/lib/koDomScrolly.js +++ b/test/client/lib/koDomScrolly.js @@ -4,8 +4,6 @@ const G = require('app/client/lib/browserGlobals').get('window', '$'); const sinon = require('sinon'); const assert = require('assert'); -/* global describe, it, after, before, beforeEach */ - describe("koDomScrolly", function() { clientUtil.setTmpMochaGlobals(); diff --git a/test/client/lib/koForm.js b/test/client/lib/koForm.js index 5883e4b1..69b187be 100644 --- a/test/client/lib/koForm.js +++ b/test/client/lib/koForm.js @@ -1,5 +1,3 @@ -/* global describe, it */ - var assert = require('chai').assert; var ko = require('knockout'); diff --git a/test/client/lib/koUtil.js b/test/client/lib/koUtil.js index e5868c13..f318929b 100644 --- a/test/client/lib/koUtil.js +++ b/test/client/lib/koUtil.js @@ -1,5 +1,3 @@ -/* global describe, it */ - var assert = require('assert'); var ko = require('knockout'); var sinon = require('sinon'); diff --git a/test/client/models/modelUtil.js b/test/client/models/modelUtil.js index 12b060e3..21e42817 100644 --- a/test/client/models/modelUtil.js +++ b/test/client/models/modelUtil.js @@ -1,5 +1,3 @@ -/* global describe, it */ - var assert = require('assert'); var ko = require('knockout'); diff --git a/test/client/models/rowset.js b/test/client/models/rowset.js index 134d4103..c010e690 100644 --- a/test/client/models/rowset.js +++ b/test/client/models/rowset.js @@ -1,5 +1,3 @@ -/* global describe, it, beforeEach */ - var _ = require('underscore'); var assert = require('chai').assert; var sinon = require('sinon'); diff --git a/test/client/models/rowuid.js b/test/client/models/rowuid.js index dd579cd0..61c8893b 100644 --- a/test/client/models/rowuid.js +++ b/test/client/models/rowuid.js @@ -1,5 +1,3 @@ -/* global describe, it */ - var assert = require('chai').assert; var rowuid = require('app/client/models/rowuid'); diff --git a/test/common/BinaryIndexedTree.js b/test/common/BinaryIndexedTree.js index 76c5d2fb..34807492 100644 --- a/test/common/BinaryIndexedTree.js +++ b/test/common/BinaryIndexedTree.js @@ -1,5 +1,3 @@ -/* global describe, before, it */ - var assert = require('assert'); var BinaryIndexedTree = require('app/common/BinaryIndexedTree'); diff --git a/test/common/CircularArray.js b/test/common/CircularArray.js index c6a91f01..43dc6598 100644 --- a/test/common/CircularArray.js +++ b/test/common/CircularArray.js @@ -1,5 +1,3 @@ -/* global describe, it */ - var assert = require('assert'); var CircularArray = require('app/common/CircularArray'); diff --git a/test/common/MemBuffer.js b/test/common/MemBuffer.js index ede4b4d7..c1497f89 100644 --- a/test/common/MemBuffer.js +++ b/test/common/MemBuffer.js @@ -1,5 +1,3 @@ -/* global describe, it */ - var assert = require('assert'); var MemBuffer = require('app/common/MemBuffer'); diff --git a/test/common/RecentItems.js b/test/common/RecentItems.js index 8d9b6935..ce03c20c 100644 --- a/test/common/RecentItems.js +++ b/test/common/RecentItems.js @@ -1,5 +1,3 @@ -/* global describe, it */ - var assert = require('chai').assert; var RecentItems = require('app/common/RecentItems'); diff --git a/test/common/arraySplice.js b/test/common/arraySplice.js index c2a7958b..11549b08 100644 --- a/test/common/arraySplice.js +++ b/test/common/arraySplice.js @@ -1,5 +1,3 @@ -/* global describe, it */ - var _ = require('underscore'); var assert = require('chai').assert; var gutil = require('app/common/gutil'); diff --git a/test/common/gutil.js b/test/common/gutil.js index f83add1f..9755513e 100644 --- a/test/common/gutil.js +++ b/test/common/gutil.js @@ -1,5 +1,3 @@ -/* global describe, it */ - var assert = require('chai').assert; var gutil = require('app/common/gutil'); var _ = require('underscore'); diff --git a/test/common/marshal.js b/test/common/marshal.js index f8cc6dc3..13c7414b 100644 --- a/test/common/marshal.js +++ b/test/common/marshal.js @@ -1,5 +1,3 @@ -/* global describe, it */ - var assert = require('chai').assert; var marshal = require('app/common/marshal'); var MemBuffer = require('app/common/MemBuffer'); diff --git a/test/common/promises.js b/test/common/promises.js index 1b1e0b46..b457d0ce 100644 --- a/test/common/promises.js +++ b/test/common/promises.js @@ -6,8 +6,6 @@ */ -/* global describe, it, before */ - var assert = require('chai').assert; var bluebird = require('bluebird'); diff --git a/test/common/serializeTiming.js b/test/common/serializeTiming.js index c6a13df8..e5f2190f 100644 --- a/test/common/serializeTiming.js +++ b/test/common/serializeTiming.js @@ -1,5 +1,3 @@ -/* global describe, it, before, after */ - var _ = require('underscore'); var assert = require('assert'); var Chance = require('chance'); diff --git a/test/common/sortTiming.js b/test/common/sortTiming.js index d00fa625..45530909 100644 --- a/test/common/sortTiming.js +++ b/test/common/sortTiming.js @@ -1,5 +1,3 @@ -/* global describe, it */ - var assert = require('assert'); var gutil = require('app/common/gutil'); var _ = require('underscore'); diff --git a/test/common/timeFormat.js b/test/common/timeFormat.js index 8d00c354..50b1fc5d 100644 --- a/test/common/timeFormat.js +++ b/test/common/timeFormat.js @@ -1,5 +1,3 @@ -/* global describe, it */ - var assert = require('assert'); var {timeFormat} = require('app/common/timeFormat'); diff --git a/test/declarations.d.ts b/test/declarations.d.ts new file mode 100644 index 00000000..8ffda614 --- /dev/null +++ b/test/declarations.d.ts @@ -0,0 +1,8 @@ +declare module "test/nbrowser/gristUtil-nbrowser"; + +// Adds missing type declaration to chai +declare namespace Chai { + interface AssertStatic { + notIncludeMembers(superset: T[], subset: T[], message?: string): void; + } +} diff --git a/test/fixtures/plugins/.jshintrc b/test/fixtures/plugins/.jshintrc new file mode 100644 index 00000000..280ab938 --- /dev/null +++ b/test/fixtures/plugins/.jshintrc @@ -0,0 +1,6 @@ +{ + "undef": true, + "unused": "vars", + "globalstrict": true, + "esnext": true +} diff --git a/test/fixtures/plugins/browserInstalledPlugins/plugins/browser-GristDocAPI/main.js b/test/fixtures/plugins/browserInstalledPlugins/plugins/browser-GristDocAPI/main.js new file mode 100644 index 00000000..60ac5959 --- /dev/null +++ b/test/fixtures/plugins/browserInstalledPlugins/plugins/browser-GristDocAPI/main.js @@ -0,0 +1,31 @@ + + +/* global grist, self */ + +self.importScripts('/grist-plugin-api.js'); + +grist.rpc.registerImpl("testApiBrowser", { + getImportSource() { + const api = grist.rpc.getStub('GristDocAPI@grist', grist.checkers.GristDocAPI); + return api.getDocName() + .then((result) => { + const content = JSON.stringify({ + tables: [{ + table_name: '', + column_metadata: [{ + id: 'getDocName', + type: 'Text' + }], + table_data: [[result]] + }] + }); + const fileItem = {content, name: "GristDocAPI.jgrist"}; + return { + item: { kind: "fileList", files: [fileItem] }, + description: "GristDocAPI results" + }; + }); + } +}); + +grist.ready(); diff --git a/test/fixtures/plugins/browserInstalledPlugins/plugins/browser-GristDocAPI/manifest.yml b/test/fixtures/plugins/browserInstalledPlugins/plugins/browser-GristDocAPI/manifest.yml new file mode 100644 index 00000000..4685dd9a --- /dev/null +++ b/test/fixtures/plugins/browserInstalledPlugins/plugins/browser-GristDocAPI/manifest.yml @@ -0,0 +1,12 @@ +name: browser-GristDocAPI +version: 0.0.0 +description: +components: + safeBrowser: main.js + +contributions: + importSources: + - importSource: + component: safeBrowser + name: testApiBrowser + label: Test GristDocAPI diff --git a/test/fixtures/plugins/browserInstalledPlugins/plugins/custom-section/index-bis.html b/test/fixtures/plugins/browserInstalledPlugins/plugins/custom-section/index-bis.html new file mode 100644 index 00000000..605f3b2b --- /dev/null +++ b/test/fixtures/plugins/browserInstalledPlugins/plugins/custom-section/index-bis.html @@ -0,0 +1,28 @@ + + +

Hello Bis

+
+ + 0

+ 1

+ 2

+ 3

+ 4

+ 5

+ 6

+ 7

+ 8

+ 9

+ 10

+ 11

+ 12

+ 13

+ 14

+ 15

+ 16

+ 17

+ 18

+ 19

+ 20

+ + diff --git a/test/fixtures/plugins/browserInstalledPlugins/plugins/custom-section/index.html b/test/fixtures/plugins/browserInstalledPlugins/plugins/custom-section/index.html new file mode 100644 index 00000000..3941c7a7 --- /dev/null +++ b/test/fixtures/plugins/browserInstalledPlugins/plugins/custom-section/index.html @@ -0,0 +1,28 @@ + + +

Hello

+
+ + 0

+ 1

+ 2

+ 3

+ 4

+ 5

+ 6

+ 7

+ 8

+ 9

+ 10

+ 11

+ 12

+ 13

+ 14

+ 15

+ 16

+ 17

+ 18

+ 19

+ 20

+ + diff --git a/test/fixtures/plugins/browserInstalledPlugins/plugins/custom-section/main.js b/test/fixtures/plugins/browserInstalledPlugins/plugins/custom-section/main.js new file mode 100644 index 00000000..335cd437 --- /dev/null +++ b/test/fixtures/plugins/browserInstalledPlugins/plugins/custom-section/main.js @@ -0,0 +1,14 @@ + + +/* globals self, grist */ + +self.importScripts("/grist-plugin-api.js"); + +class CustomSection { + createSection(renderTarget) { + return grist.api.render('index.html', renderTarget); + } +} + +grist.rpc.registerImpl('hello', new CustomSection(), grist.CustomSectionDescription); +grist.ready(); diff --git a/test/fixtures/plugins/browserInstalledPlugins/plugins/custom-section/manifest.yml b/test/fixtures/plugins/browserInstalledPlugins/plugins/custom-section/manifest.yml new file mode 100644 index 00000000..66ddad2f --- /dev/null +++ b/test/fixtures/plugins/browserInstalledPlugins/plugins/custom-section/manifest.yml @@ -0,0 +1,12 @@ +name: helloSection +version: 0.0.0 +components: + safeBrowser: main.js +contributions: + customSections: + - path: index.html + name: Hello World + - path: index-bis.html + name: Hello World (bis) + - path: test-subscribe-api.html + name: dataAPI test diff --git a/test/fixtures/plugins/browserInstalledPlugins/plugins/custom-section/test-subscribe-api.html b/test/fixtures/plugins/browserInstalledPlugins/plugins/custom-section/test-subscribe-api.html new file mode 100644 index 00000000..5a62f7d7 --- /dev/null +++ b/test/fixtures/plugins/browserInstalledPlugins/plugins/custom-section/test-subscribe-api.html @@ -0,0 +1,11 @@ + + + + + + + +

Data API

+
+ + diff --git a/test/fixtures/plugins/browserInstalledPlugins/plugins/custom-section/test-subscribe-api.js b/test/fixtures/plugins/browserInstalledPlugins/plugins/custom-section/test-subscribe-api.js new file mode 100644 index 00000000..e8e929bf --- /dev/null +++ b/test/fixtures/plugins/browserInstalledPlugins/plugins/custom-section/test-subscribe-api.js @@ -0,0 +1,36 @@ + + +/* global grist, window, $, document */ +let tableId = 'Table1'; + +grist.ready(); +grist.api.subscribe(tableId); + +window.onload = () => { + showColumn('A'); +}; + +grist.rpc.on("message", (msg) => { + if (msg.type === "docAction") { + // There could by many doc actions and fetching table is expensive, in practice this call would + // be be throttle + if (msg.action[0] === 'RenameTable') { + tableId = msg.action[2]; + } + showColumn('A'); + } +}); + +// fetch table and call the view with values of coldId +function showColumn(colId) { + grist.docApi.fetchTable(tableId).then(cols => updateView(cols[colId])); +} + +// show the first column +function updateView(values) { + $("#panel").empty(); + const res = $('
'); + const text = document.createTextNode(JSON.stringify(values)); + res.append(text); + $("#panel").append(res); +} diff --git a/test/fixtures/plugins/browserInstalledPlugins/plugins/dummy-importer/index.html b/test/fixtures/plugins/browserInstalledPlugins/plugins/dummy-importer/index.html new file mode 100644 index 00000000..07a6fc70 --- /dev/null +++ b/test/fixtures/plugins/browserInstalledPlugins/plugins/dummy-importer/index.html @@ -0,0 +1,22 @@ + + + + + + + + + + + + +
+ "name of the file: " + + + + diff --git a/test/fixtures/plugins/browserInstalledPlugins/plugins/dummy-importer/main.js b/test/fixtures/plugins/browserInstalledPlugins/plugins/dummy-importer/main.js new file mode 100644 index 00000000..a44e8d9a --- /dev/null +++ b/test/fixtures/plugins/browserInstalledPlugins/plugins/dummy-importer/main.js @@ -0,0 +1,9 @@ + + +/* global grist, self */ + +self.importScripts('/grist-plugin-api.js'); + +grist.addImporter('dummy', 'index.html', 'fullscreen'); +grist.addImporter('dummy-inlined', 'index.html', 'inline'); +grist.ready(); diff --git a/test/fixtures/plugins/browserInstalledPlugins/plugins/dummy-importer/manifest.yml b/test/fixtures/plugins/browserInstalledPlugins/plugins/dummy-importer/manifest.yml new file mode 100644 index 00000000..7ea06a35 --- /dev/null +++ b/test/fixtures/plugins/browserInstalledPlugins/plugins/dummy-importer/manifest.yml @@ -0,0 +1,16 @@ +name: pluginName +version: 0.0.1 +components: + safeBrowser: main.js + safePython: sandbox/main.py + unsafeNode: node/main.js +contributions: + importSources: + - importSource: + component: safeBrowser + name: dummy + label: Dummy importer + - importSource: + component: safeBrowser + name: dummy-inlined + label: Inline Importer diff --git a/test/fixtures/plugins/browserInstalledPlugins/plugins/dummy-importer/node/main.js b/test/fixtures/plugins/browserInstalledPlugins/plugins/dummy-importer/node/main.js new file mode 100644 index 00000000..67a2e18e --- /dev/null +++ b/test/fixtures/plugins/browserInstalledPlugins/plugins/dummy-importer/node/main.js @@ -0,0 +1,4 @@ +const grist = require('grist-plugin-api'); + +grist.rpc.registerFunc("func1", (name) => `Yo: ${name}`); +grist.ready(); diff --git a/test/fixtures/plugins/browserInstalledPlugins/plugins/dummy-importer/sandbox/main.py b/test/fixtures/plugins/browserInstalledPlugins/plugins/dummy-importer/sandbox/main.py new file mode 100644 index 00000000..9d599819 --- /dev/null +++ b/test/fixtures/plugins/browserInstalledPlugins/plugins/dummy-importer/sandbox/main.py @@ -0,0 +1,11 @@ +import sandbox + +def greet(val): + return "With love: " + val + +def main(): + sandbox.register("func1", greet) + sandbox.run() + +if __name__ == "__main__": + main() diff --git a/test/fixtures/plugins/browserInstalledPlugins/plugins/dummy-importer/script.js b/test/fixtures/plugins/browserInstalledPlugins/plugins/dummy-importer/script.js new file mode 100644 index 00000000..64745491 --- /dev/null +++ b/test/fixtures/plugins/browserInstalledPlugins/plugins/dummy-importer/script.js @@ -0,0 +1,44 @@ + + +/* global grist, window, document, $ */ + +let resolve; // eslint-disable-line no-unused-vars + +const importer = { + getImportSource: () => new Promise((_resolve) => { + resolve = _resolve; + }) +}; + +grist.rpc.registerImpl('dummy', importer ); +grist.rpc.registerImpl('dummy-inlined', importer ); + +grist.ready(); + +window.onload = function() { + callFunctionOnClick('#call-safePython', 'func1@sandbox/main.py', 'Bob'); + callFunctionOnClick('#call-unsafeNode', 'func1@node/main.js', 'Alice'); + document.querySelector('#cancel').addEventListener('click', () => resolve()); + document.querySelector('#ok').addEventListener('click', () => { + const name = $('#name').val(); + resolve({ + item: { + kind: "fileList", + files: [{content: "A,B\n1,2\n", name}] + }, + description: name + " selected!" + }); + }); +}; + +function callFunctionOnClick(selector, funcName, ...args) { + document.querySelector(selector).addEventListener('click', () => { + grist.rpc.callRemoteFunc(funcName, ...args) + .then(val => { + const resElement = document.createElement('h1'); + resElement.classList.add(`result`); + resElement.textContent = val; + document.body.appendChild(resElement); + }); + }); +} diff --git a/test/fixtures/plugins/builtInPlugins/plugins/2/manifest.yml b/test/fixtures/plugins/builtInPlugins/plugins/2/manifest.yml new file mode 100644 index 00000000..ae4c681a --- /dev/null +++ b/test/fixtures/plugins/builtInPlugins/plugins/2/manifest.yml @@ -0,0 +1,3 @@ +version: 0.0.1 +contributions: + importSources: diff --git a/test/fixtures/plugins/builtInPlugins/plugins/experimental-plugin/manifest.yml b/test/fixtures/plugins/builtInPlugins/plugins/experimental-plugin/manifest.yml new file mode 100644 index 00000000..a11a634a --- /dev/null +++ b/test/fixtures/plugins/builtInPlugins/plugins/experimental-plugin/manifest.yml @@ -0,0 +1,12 @@ +name: crazy-plugin +version: 0.0.1 +experimental: true +components: + safePython: sandbox/main.py + +contributions: + fileParsers: + - fileExtensions: ["csv"] + parseFile: + component: "safePython" + name: "csv_parser" diff --git a/test/fixtures/plugins/builtInPlugins/plugins/experimental-plugin/sandbox/main.py b/test/fixtures/plugins/builtInPlugins/plugins/experimental-plugin/sandbox/main.py new file mode 100644 index 00000000..556df42e --- /dev/null +++ b/test/fixtures/plugins/builtInPlugins/plugins/experimental-plugin/sandbox/main.py @@ -0,0 +1 @@ +# nothing diff --git a/test/fixtures/plugins/builtInPlugins/plugins/invalid-contrib-point/manifest.yml b/test/fixtures/plugins/builtInPlugins/plugins/invalid-contrib-point/manifest.yml new file mode 100644 index 00000000..490870ac --- /dev/null +++ b/test/fixtures/plugins/builtInPlugins/plugins/invalid-contrib-point/manifest.yml @@ -0,0 +1,3 @@ +version: 0.0.1 +contributions: + invalidContibutionPoint: diff --git a/test/fixtures/plugins/builtInPlugins/plugins/long-call/manifest.yml b/test/fixtures/plugins/builtInPlugins/plugins/long-call/manifest.yml new file mode 100644 index 00000000..dc54e8f4 --- /dev/null +++ b/test/fixtures/plugins/builtInPlugins/plugins/long-call/manifest.yml @@ -0,0 +1,14 @@ +name: pluginName +version: 0.0.1 +components: + safePython: sandbox/main.py + deactivate: + # Let's keep it low for tests to be fast, but big enough for test to be accurate. + inactivitySec: 0.1 + +contributions: + fileParsers: + - fileExtensions: ["csv"] + parseFile: + component: "safePython" + name: "csv_parser" diff --git a/test/fixtures/plugins/builtInPlugins/plugins/long-call/sandbox/main.py b/test/fixtures/plugins/builtInPlugins/plugins/long-call/sandbox/main.py new file mode 100644 index 00000000..b1bc9d95 --- /dev/null +++ b/test/fixtures/plugins/builtInPlugins/plugins/long-call/sandbox/main.py @@ -0,0 +1,28 @@ +import time +import sandbox + +# pylint: disable=unused-argument +# pylint: disable=no-member + +def import_files(file_source, parse_options): + end = time.time() + 1 + while time.time() < end: + pass + return { + "parseOptions": {}, + # Make sure the output is a list of GristTables as documented at app/plugin/GristTable.ts + "tables": [{ + "table_name": "mytable", + "column_metadata": [], + "table_data": [], + }] + } + + +def main(): + sandbox.register("csv_parser.parseFile", import_files) + sandbox.run() # pylint: disable=no-member + + +if __name__ == "__main__": + main() diff --git a/test/fixtures/plugins/builtInPlugins/plugins/missing-component/manifest.yml b/test/fixtures/plugins/builtInPlugins/plugins/missing-component/manifest.yml new file mode 100644 index 00000000..091a0df8 --- /dev/null +++ b/test/fixtures/plugins/builtInPlugins/plugins/missing-component/manifest.yml @@ -0,0 +1,9 @@ +name: missing-components +version: 0.0.1 +# missing `components` entry +contributions: + fileParsers: + - fileExtensions: ["csv"] + parseFile: + component: "safePython" + name: "csv_parser" diff --git a/test/fixtures/plugins/builtInPlugins/plugins/missing-safePython/manifest.yml b/test/fixtures/plugins/builtInPlugins/plugins/missing-safePython/manifest.yml new file mode 100644 index 00000000..96925f26 --- /dev/null +++ b/test/fixtures/plugins/builtInPlugins/plugins/missing-safePython/manifest.yml @@ -0,0 +1,10 @@ +name: missing-safePython +version: 0.0.1 +components: + # missing `safePython` component +contributions: + fileParsers: + - fileExtensions: ["csv"] + parseFile: + component: "safePython" + name: "csv_parser" diff --git a/test/fixtures/plugins/builtInPlugins/plugins/safePython-deactivate-fast/manifest.yml b/test/fixtures/plugins/builtInPlugins/plugins/safePython-deactivate-fast/manifest.yml new file mode 100644 index 00000000..dc54e8f4 --- /dev/null +++ b/test/fixtures/plugins/builtInPlugins/plugins/safePython-deactivate-fast/manifest.yml @@ -0,0 +1,14 @@ +name: pluginName +version: 0.0.1 +components: + safePython: sandbox/main.py + deactivate: + # Let's keep it low for tests to be fast, but big enough for test to be accurate. + inactivitySec: 0.1 + +contributions: + fileParsers: + - fileExtensions: ["csv"] + parseFile: + component: "safePython" + name: "csv_parser" diff --git a/test/fixtures/plugins/builtInPlugins/plugins/safePython-deactivate-fast/sandbox/main.py b/test/fixtures/plugins/builtInPlugins/plugins/safePython-deactivate-fast/sandbox/main.py new file mode 100644 index 00000000..f83f3685 --- /dev/null +++ b/test/fixtures/plugins/builtInPlugins/plugins/safePython-deactivate-fast/sandbox/main.py @@ -0,0 +1,28 @@ +import sandbox + +# pylint: disable=unused-argument +# pylint: disable=no-member + +# TODO: configure pylint behavior for both `test/fixtures/plugins` and +# `/plugins` folders: either to ignore them completely or to ignore +# above mentioned rules. + +def import_files(file_source, parse_options=None): + return { + "parseOptions": {}, + "tables": [{ + "table_name": "mytable", + "column_metadata": [], + "table_data": [] + }]} + + +def main(): + # Todo: Grist should expose a register method accepting arguments as + # follow: register('csv_parser', 'importFiles', can_parse) + sandbox.register("csv_parser.parseFile", import_files) + sandbox.run() # pylint: disable=no-member + + +if __name__ == "__main__": + main() diff --git a/test/fixtures/plugins/builtInPlugins/plugins/testing-function-call-plugin/backend.js b/test/fixtures/plugins/builtInPlugins/plugins/testing-function-call-plugin/backend.js new file mode 100644 index 00000000..816ee3f7 --- /dev/null +++ b/test/fixtures/plugins/builtInPlugins/plugins/testing-function-call-plugin/backend.js @@ -0,0 +1,5 @@ +const grist = require('grist-plugin-api'); + +grist.rpc.registerFunc("yo", (name) => `yo ${name}`); +grist.rpc.registerFunc("yoSafePython", (name) => grist.rpc.callRemoteFunc("yo@sandbox/main.py", name)); +grist.ready(); diff --git a/test/fixtures/plugins/builtInPlugins/plugins/testing-function-call-plugin/manifest.yml b/test/fixtures/plugins/builtInPlugins/plugins/testing-function-call-plugin/manifest.yml new file mode 100644 index 00000000..c9c15e37 --- /dev/null +++ b/test/fixtures/plugins/builtInPlugins/plugins/testing-function-call-plugin/manifest.yml @@ -0,0 +1,17 @@ +name: testPluginFunction +version: 0.0.1 +components: + safePython: sandbox/main.py + unsafeNode: backend.js + safeBrowser: main.js + +# For the purpose of this unit-test contributions property is actually +# NOT need and only provided for the sake of making this manifest +# valid, + +contributions: + importSources: + - importSource: + component: "safeBrowser" + name: index.html + label: My safe importer diff --git a/test/fixtures/plugins/builtInPlugins/plugins/testing-function-call-plugin/sandbox/main.py b/test/fixtures/plugins/builtInPlugins/plugins/testing-function-call-plugin/sandbox/main.py new file mode 100644 index 00000000..2990989d --- /dev/null +++ b/test/fixtures/plugins/builtInPlugins/plugins/testing-function-call-plugin/sandbox/main.py @@ -0,0 +1,15 @@ +import sandbox + +def greet(name): + return "Hi " + name + +def yo(name): + return "yo " + name + " from safePython" + +def main(): + sandbox.register("greet", greet) + sandbox.register("yo", yo) + sandbox.run() + +if __name__ == "__main__": + main() diff --git a/test/fixtures/plugins/builtInPlugins/plugins/valid-file-parser/manifest.yml b/test/fixtures/plugins/builtInPlugins/plugins/valid-file-parser/manifest.yml new file mode 100644 index 00000000..00595d7e --- /dev/null +++ b/test/fixtures/plugins/builtInPlugins/plugins/valid-file-parser/manifest.yml @@ -0,0 +1,11 @@ +name: pluginName +version: 0.0.1 +components: + safePython: sandbox/main.py + +contributions: + fileParsers: + - fileExtensions: ["csv"] + parseFile: + component: "safePython" + name: "csv_parser" diff --git a/test/fixtures/plugins/builtInPlugins/plugins/valid-file-parser/sandbox/main.py b/test/fixtures/plugins/builtInPlugins/plugins/valid-file-parser/sandbox/main.py new file mode 100644 index 00000000..9b9a4d9d --- /dev/null +++ b/test/fixtures/plugins/builtInPlugins/plugins/valid-file-parser/sandbox/main.py @@ -0,0 +1,29 @@ +import sandbox + +# pylint: disable=unused-argument +# pylint: disable=no-member + +# TODO: configure pylint behavior for both `test/fixtures/plugins` and +# `/plugins` folders: either to ignore them completely or to ignore +# above mentioned rules. + +def import_files(file_source, parse_options): + parse_options.update({"NUM_ROWS" : 1}) + return { + "parseOptions": parse_options, + "tables": [{ + "table_name": "mytable", + "column_metadata": [], + "table_data": [] + }]} + + +def main(): + # Todo: Grist should expose a register method accepting arguments as + # follow: register('csv_parser', 'parseFile', can_parse) + sandbox.register("csv_parser.parseFile", import_files) + sandbox.run() # pylint: disable=no-member + + +if __name__ == "__main__": + main() diff --git a/test/fixtures/plugins/builtInPlugins/plugins/valid-import-source/manifest.yml b/test/fixtures/plugins/builtInPlugins/plugins/valid-import-source/manifest.yml new file mode 100644 index 00000000..9ff8e565 --- /dev/null +++ b/test/fixtures/plugins/builtInPlugins/plugins/valid-import-source/manifest.yml @@ -0,0 +1,10 @@ +name: pluginName +version: 0.0.1 +components: + safeBrowser: '.' +contributions: + importSources: + - importSource: + component: "safeBrowser" + name: index.html + label: My safe importer diff --git a/test/fixtures/plugins/builtInPlugins/plugins/wrong-json/manifest.json b/test/fixtures/plugins/builtInPlugins/plugins/wrong-json/manifest.json new file mode 100644 index 00000000..69d033b1 --- /dev/null +++ b/test/fixtures/plugins/builtInPlugins/plugins/wrong-json/manifest.json @@ -0,0 +1 @@ +wrong manifest as well in json diff --git a/test/fixtures/plugins/builtInPlugins/plugins/wrong-yaml/manifest.yml b/test/fixtures/plugins/builtInPlugins/plugins/wrong-yaml/manifest.yml new file mode 100644 index 00000000..962d3f27 --- /dev/null +++ b/test/fixtures/plugins/builtInPlugins/plugins/wrong-yaml/manifest.yml @@ -0,0 +1 @@ +:some-wrong-manifest diff --git a/test/fixtures/plugins/installedPlugins/plugins/node-GristDocAPI/TestSubscribe.js b/test/fixtures/plugins/installedPlugins/plugins/node-GristDocAPI/TestSubscribe.js new file mode 100644 index 00000000..338631f2 --- /dev/null +++ b/test/fixtures/plugins/installedPlugins/plugins/node-GristDocAPI/TestSubscribe.js @@ -0,0 +1,34 @@ +const grist = require('grist-plugin-api'); + +const {foo} = grist.rpc.getStub('foo@grist'); +let tableId = 'Table1'; +const colId = 'A'; +let promise = Promise.resolve(true); + +grist.rpc.on('message', msg => { + if (msg.type === "docAction") { + if (msg.action[0] === 'RenameTable') { + tableId = msg.action[2]; + } + promise = getColValues(colId).then(foo); + } +}); + +function getColValues(colId) { + return grist.docApi.fetchTable(tableId).then(data => data[colId]); +} + +class TestSubscribe { + + invoke(api, name, args){ + return grist[api][name](...args); + } + + // Returns a promise that resolves when an ongoing call resolves. Resolves right-awa if plugin has + // no pending call. + waitForPlugin() { + return promise.then(() => true); + } +} + +module.exports = TestSubscribe; diff --git a/test/fixtures/plugins/installedPlugins/plugins/node-GristDocAPI/main.js b/test/fixtures/plugins/installedPlugins/plugins/node-GristDocAPI/main.js new file mode 100644 index 00000000..1d128dc5 --- /dev/null +++ b/test/fixtures/plugins/installedPlugins/plugins/node-GristDocAPI/main.js @@ -0,0 +1,21 @@ +const grist = require('grist-plugin-api'); +const TestSubscribe = require('./TestSubscribe'); + +grist.rpc.registerImpl("testApiNode", { // todo rename to testGristDocApiNode + invoke: (name, args) => { + const api = grist.rpc.getStub("GristDocAPI@grist", grist.checkers.GristDocAPI); + return api[name](...args) + .then((result) => [`node-GristDocAPI ${name}(${args.join(",")})`, result]); + }, +}); + +grist.rpc.registerImpl("testDocStorage", { + invoke: (name, args) => { + const api = grist.rpc.getStub("DocStorage@grist", grist.checkers.Storage); + return api[name](...args); + }, +}); + +grist.rpc.registerImpl("testSubscribe", new TestSubscribe()); + +grist.ready(); diff --git a/test/fixtures/plugins/installedPlugins/plugins/node-GristDocAPI/manifest.yml b/test/fixtures/plugins/installedPlugins/plugins/node-GristDocAPI/manifest.yml new file mode 100644 index 00000000..09adf486 --- /dev/null +++ b/test/fixtures/plugins/installedPlugins/plugins/node-GristDocAPI/manifest.yml @@ -0,0 +1,7 @@ +name: node-GristDocAPI +version: 0.0.0 +description: +components: + unsafeNode: main.js + +contributions: {} diff --git a/test/fixtures/plugins/installedPlugins/plugins/node-fail/main.js b/test/fixtures/plugins/installedPlugins/plugins/node-fail/main.js new file mode 100644 index 00000000..6fb42f86 --- /dev/null +++ b/test/fixtures/plugins/installedPlugins/plugins/node-fail/main.js @@ -0,0 +1,3 @@ + + +// die immediately. diff --git a/test/fixtures/plugins/installedPlugins/plugins/node-fail/manifest.yml b/test/fixtures/plugins/installedPlugins/plugins/node-fail/manifest.yml new file mode 100644 index 00000000..fec3a5d0 --- /dev/null +++ b/test/fixtures/plugins/installedPlugins/plugins/node-fail/manifest.yml @@ -0,0 +1,11 @@ +name: node-fail +version: 0.0.0 +description: +components: + unsafeNode: main.js +contributions: + fileParsers: + - fileExtensions: ["csv"] + parseFile: + component: unsafeNode + name: node-fail diff --git a/test/fixtures/plugins/installedPlugins/plugins/node-mini-csv/manifest.yml b/test/fixtures/plugins/installedPlugins/plugins/node-mini-csv/manifest.yml new file mode 100644 index 00000000..28e77c02 --- /dev/null +++ b/test/fixtures/plugins/installedPlugins/plugins/node-mini-csv/manifest.yml @@ -0,0 +1,11 @@ +name: minicsv +version: 0.0.0 +description: minicsv +components: + unsafeNode: nodebox/main.js +contributions: + fileParsers: + - fileExtensions: ["csv"] + parseFile: + component: unsafeNode + name: MiniCSV diff --git a/test/fixtures/plugins/installedPlugins/plugins/node-mini-csv/nodebox/main.js b/test/fixtures/plugins/installedPlugins/plugins/node-mini-csv/nodebox/main.js new file mode 100644 index 00000000..43b2aad7 --- /dev/null +++ b/test/fixtures/plugins/installedPlugins/plugins/node-mini-csv/nodebox/main.js @@ -0,0 +1,69 @@ +/** + * + * A minimal CSV reader with no type detection. + * All communication done by hand - real plugins should have helper code for + * RPC. + * + */ + +const csv = require('csv'); +const fs = require('fs'); +const path = require('path'); + +function readCsv(data, replier) { + csv.parse(data, {}, function(err, output) { + const result = { + parseOptions: { + options: "" + }, + tables: [ + { + table_name: "space-monkey" + require('dependency_test'), + column_metadata: output[0].map(name => { + return { + id: name, + type: 'Text' + }; + }), + table_data: output[0].map((name, idx) => { + return output.slice(1).map(row => row[idx]); + }) + } + ] + }; + replier(result); + }); +} + +function processMessage(msg, replier, error_replier) { + if (msg.meth == 'parseFile') { + var dir = msg.dir; + var fname = msg.args[0].path; + var data = fs.readFileSync(path.resolve(dir, fname)); + readCsv(data, replier); + } else { + error_replier('unknown method'); + } +} + +process.on('message', (m) => { + const sendReply = (result) => { + process.send({ + mtype: 2, /* RespData */ + reqId: m.reqId, + data: result + }); + }; + const sendError = (txt) => { + process.send({ + mtype: 3, /* RespErr */ + reqId: m.reqId, + mesg: txt + }); + }; + processMessage(m, sendReply, sendError); +}); + +// Once we have a handler for 'message' set up, send home a ready +// message to give the all-clear. +process.send({ mtype: 4, data: {ready: true }}); diff --git a/test/fixtures/plugins/installedPlugins/plugins/node-mini-csv/nodebox/node_modules/dependency_test/index.js b/test/fixtures/plugins/installedPlugins/plugins/node-mini-csv/nodebox/node_modules/dependency_test/index.js new file mode 100644 index 00000000..93a3fc2f --- /dev/null +++ b/test/fixtures/plugins/installedPlugins/plugins/node-mini-csv/nodebox/node_modules/dependency_test/index.js @@ -0,0 +1,3 @@ +"use strict"; + +module.exports = 42; diff --git a/test/fixtures/plugins/installedPlugins/plugins/node-mini-csv/nodebox/node_modules/dependency_test/package.json b/test/fixtures/plugins/installedPlugins/plugins/node-mini-csv/nodebox/node_modules/dependency_test/package.json new file mode 100644 index 00000000..bc8cc9f1 --- /dev/null +++ b/test/fixtures/plugins/installedPlugins/plugins/node-mini-csv/nodebox/node_modules/dependency_test/package.json @@ -0,0 +1,11 @@ +{ + "name": "dependency_test", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC" +} diff --git a/test/fixtures/plugins/installedPlugins/plugins/node-wrong-message/main.js b/test/fixtures/plugins/installedPlugins/plugins/node-wrong-message/main.js new file mode 100644 index 00000000..54a4ce19 --- /dev/null +++ b/test/fixtures/plugins/installedPlugins/plugins/node-wrong-message/main.js @@ -0,0 +1,3 @@ + + +process.send({ greeny: true }); diff --git a/test/fixtures/plugins/installedPlugins/plugins/node-wrong-message/manifest.yml b/test/fixtures/plugins/installedPlugins/plugins/node-wrong-message/manifest.yml new file mode 100644 index 00000000..f9156585 --- /dev/null +++ b/test/fixtures/plugins/installedPlugins/plugins/node-wrong-message/manifest.yml @@ -0,0 +1,11 @@ +name: node-wrong-message +version: 0.0.0 +description: +components: + unsafeNode: main.js +contributions: + fileParsers: + - fileExtensions: ["csv"] + parseFile: + component: unsafeNode + name: node-wrong-message diff --git a/test/fixtures/plugins/installedPlugins/plugins/valid-import-source/manifest.yml b/test/fixtures/plugins/installedPlugins/plugins/valid-import-source/manifest.yml new file mode 100644 index 00000000..4482468d --- /dev/null +++ b/test/fixtures/plugins/installedPlugins/plugins/valid-import-source/manifest.yml @@ -0,0 +1,10 @@ +name: validPluginName +version: 0.0.1 +components: + safeBrowser: '.' +contributions: + importSources: + - importSource: + component: "safeBrowser" + name: index.html + label: My custom safe importer diff --git a/test/init-mocha-webdriver.js b/test/init-mocha-webdriver.js index dc3faa7f..01745d1b 100644 --- a/test/init-mocha-webdriver.js +++ b/test/init-mocha-webdriver.js @@ -10,7 +10,7 @@ // Increase the threshold since the default (of 40 characters) is often too low. // You can override it using CHAI_TRUNCATE_THRESHOLD env var; 0 disables it. require('chai').config.truncateThreshold = process.env.CHAI_TRUNCATE_THRESHOLD ? - parseFloat(process.env.CHAI_TRUNCATE_THRESHOLD) : 200; + parseFloat(process.env.CHAI_TRUNCATE_THRESHOLD) : 4000; // Set an explicit window size (if not set by an external variable), to ensure that manully-run // and Jenkins-run tests, headless or not, use a consistent size. (Not that height is still not @@ -44,3 +44,22 @@ if (process.env.MOCHA_WEBDRIVER_IGNORE_CHROME_VERSION === undefined) { if (process.env.MOCHA_WEBDRIVER_NO_CONTROL_BANNER === undefined) { process.env.MOCHA_WEBDRIVER_NO_CONTROL_BANNER = "1"; } + +// Detect whether there is an nbrowser test. If so, +// set an environment variable that will be available +// in individual processes if --parallel is enabled. +for (const arg of process.argv) { + if (arg.includes('/nbrowser/')) { + process.env.MOCHA_WEBDRIVER = '1'; + } +} + +// If --parallel is enabled, and we are in an individual +// worker process, set up mochaHooks. Watch out: at the +// time of writing, there's no way to have hooks run at the +// start and end of the worker process. +if (process.env.MOCHA_WORKER_ID !== undefined && + process.env.MOCHA_WEBDRIVER !== undefined) { + const {getMochaHooks} = require('mocha-webdriver'); + exports.mochaHooks = getMochaHooks(); +} diff --git a/test/nbrowser/ClientUnitTests.ntest.js b/test/nbrowser/ClientUnitTests.ntest.js new file mode 100644 index 00000000..4b4f542a --- /dev/null +++ b/test/nbrowser/ClientUnitTests.ntest.js @@ -0,0 +1,24 @@ +import { driver } from 'mocha-webdriver'; +import { $, gu, server, test } from 'test/nbrowser/gristUtil-nbrowser'; + +describe('ClientUnitTests.ntest', function() { + test.setupTestSuite(this); + + before(async function() { + await gu.supportOldTimeyTestCode(); + var timingTests = process.env.ENABLE_TIMING_TESTS ? 1 : 0; + await driver.get(server.getHost() + '/v/gtag/test.html?timing=' + timingTests); + }); + + it('should reach 100% with no failures', async function() { + this.timeout(30000); // You've got 30 seconds + + await $('#mocha-status:contains(DONE)').wait(); + + const failures = await driver.executeScript('return mocha.failedTests;'); + if (failures.length > 0) { + var listing = failures.map(fail => fail.title + ': ' + fail.error).join("\n"); + throw new Error("Browser returned " + failures.length + " failed tests:\n" + listing); + } + }); +}); diff --git a/test/nbrowser/CodeEditor.ntest.js b/test/nbrowser/CodeEditor.ntest.js new file mode 100644 index 00000000..86c828a5 --- /dev/null +++ b/test/nbrowser/CodeEditor.ntest.js @@ -0,0 +1,61 @@ +/* global window */ + +import { assert, driver } from 'mocha-webdriver'; +import { $, gu, test } from 'test/nbrowser/gristUtil-nbrowser'; + +describe('CodeEditor.ntest', function() { + const cleanup = test.setupTestSuite(this); + + before(async function() { + await gu.supportOldTimeyTestCode(); + await gu.useFixtureDoc(cleanup, '../uploads/CodeEditor.test.csv', true); + }); + + afterEach(function() { + return gu.checkForErrors(); + }); + + it('Should activate on click of `Code View` button', async function() { + await gu.openSidePane('code'); + assert.match(await $('.g-code-viewer').wait().getText(), + /class CodeEditor_test:[^]*A = grist.Text\(\)[^]*B = grist.Numeric\(\)/); + }); + + it('Should update to reflect changes in schema', async function() { + await gu.actions.selectTabView('CodeEditor.test'); + // open the side menu + await gu.openSidePane('field'); + + await gu.getCellRC(0, 0).click(); + await $(".test-field-label").wait(assert.isDisplayed); + await $(".test-field-label").sendNewText('foo'); + await gu.waitForServer(); + + await gu.getCellRC(0, 1).click(); + await $(".test-field-label").sendNewText('bar'); + await gu.waitForServer(); // Must wait for colId change to finish + + await gu.setType('Reference'); + await gu.applyTypeConversion(); + await gu.setVisibleCol('foo'); + await gu.waitForServer(); + + // Check that type conversion worked correctly. + assert.equal(await gu.getCellRC(1, 1).text(), 'Bob'); + + await gu.openSidePane('code'); + assert.match(await $('.g-code-viewer').wait().getText(), + /foo = grist.Text\(\)[^]*bar = grist.Reference\('CodeEditor_test'\)/); + }); + + it('should filter out helper columns', async function() { + assert.notInclude(await $('.g-code-viewer').wait().getText(), 'gristHelper'); + }); + + it('should allow text selection', async function() { + const textElem = $('.hljs-title:contains(CodeEditor)'); + await textElem.click(); + await driver.withActions(a => a.doubleClick(textElem.elem())); + assert.equal(await driver.executeScript(() => window.getSelection().toString()), 'CodeEditor_test'); + }); +}); diff --git a/test/nbrowser/ColumnOps.ntest.js b/test/nbrowser/ColumnOps.ntest.js new file mode 100644 index 00000000..816d7bbb --- /dev/null +++ b/test/nbrowser/ColumnOps.ntest.js @@ -0,0 +1,276 @@ +import { assert, driver } from 'mocha-webdriver'; +import { $, gu, test } from 'test/nbrowser/gristUtil-nbrowser'; + +const colHeaderScrollOpts = {block: "start", inline: "end"}; + +describe('ColumnOps.ntest', function() { + const cleanup = test.setupTestSuite(this); + + before(async function() { + this.timeout(Math.max(this.timeout(), 30000)); // Long-running test, unfortunately + await gu.supportOldTimeyTestCode(); + await gu.useFixtureDoc(cleanup, "World.grist", true); + await gu.toggleSidePanel('left', 'close'); + }); + + afterEach(function() { + return gu.checkForErrors(); + }); + + it("should allow adding and deleting columns", async function() { + await gu.clickColumnMenuItem('Name', 'Insert column to the right'); + await gu.waitForServer(); + // Newly created columns labels become editable automatically. The next line checks that the + // label is editable and then closes the editor. + await gu.userActionsCollect(true); + await gu.getOpenEditingLabel(await gu.getColumnHeader('A')).wait().sendKeys($.ENTER); + // Verify that no UserActions were actually sent. + await gu.waitForServer(); + await gu.userActionsVerify([]); + await gu.userActionsCollect(false); + await gu.waitAppFocus(true); + await assert.isPresent(gu.getColumnHeader('A'), true); + await assert.isPresent(gu.getOpenEditingLabel(gu.getColumnHeader('A')), false); + + await gu.clickColumnMenuItem('A', 'Delete column'); + await gu.waitForServer(); + await assert.isPresent(gu.getColumnHeader('A'), false); + }); + + it("should allow adding columns with new column menu", async function() { + await assert.isPresent(gu.getColumnHeader('A'), false); + await $('.mod-add-column').scrollIntoView(true); + await $('.mod-add-column').click(); + await gu.actions.selectFloatingOption('Add Column'); + await gu.userActionsCollect(true); + await gu.waitToPass(() => gu.getColumnHeader('A')); + await gu.getOpenEditingLabel(await gu.getColumnHeader('A')).wait().sendKeys($.ENTER); + await gu.waitForServer(); + await gu.userActionsVerify([["AddRecord", "_grist_Views_section_field", null, + {"colRef":43, "parentId":4, "parentPos":null}]]); + await gu.userActionsCollect(false); + await gu.waitAppFocus(true); + await assert.isPresent(gu.getColumnHeader('A'), true); + await assert.isPresent(gu.getOpenEditingLabel(gu.getColumnHeader('A')), false); + assert.deepEqual(await gu.getGridLabels('City'), + ["Name", "Country", "District", "Population", "A"]); + }); + + it("should allow hiding columns", async function() { + await assert.isPresent(gu.getColumnHeader('Name'), true); + await gu.getColumnHeader('Name').scrollIntoView(colHeaderScrollOpts); + await gu.clickColumnMenuItem('Name', 'Hide column'); + await gu.waitForServer(); + await assert.isPresent(gu.getColumnHeader('Name'), false); + + await $(".test-undo").click(); + await gu.waitForServer(); + await assert.isPresent(gu.getColumnHeader('Name'), true); + }); + + it("[+] button should allow showing hidden columns", async function() { + // Hide a column first + await assert.isPresent(gu.getColumnHeader('Name'), true); + await gu.getColumnHeader('Name').scrollIntoView(colHeaderScrollOpts); + await gu.clickColumnMenuItem('Name', 'Hide column'); + await gu.waitForServer(); + await assert.isPresent(gu.getColumnHeader('Name'), false); + + // Then show it using the add column menu + await $('.mod-add-column').scrollIntoView(true); + await $(".mod-add-column").click(); + await gu.actions.selectFloatingOption('Show column Name'); + await gu.waitForServer(); + await assert.isPresent(gu.getColumnHeader('Name'), true); + }); + + it("[+] button show add column directly if no hidden columns", async function() { + await $('.mod-add-column').scrollIntoView(true); + await $(".mod-add-column").click(); + await gu.actions.selectFloatingOption('Show column Pop'); + await gu.waitForServer(); + await assert.isPresent(gu.getColumnHeader("Pop. '000"), true); + + await assert.isPresent(gu.getColumnHeader('B'), false); + await $('.mod-add-column').scrollIntoView(true); + await $(".mod-add-column").click(); + await gu.waitToPass(() => gu.getColumnHeader('B')); + await gu.getOpenEditingLabel(await gu.getColumnHeader('B')).wait().sendKeys($.ENTER); + await gu.waitForServer(); + await gu.waitAppFocus(true); + await assert.isPresent(gu.getColumnHeader('B'), true); + }); + + it("should allow renaming columns", async function() { + await gu.getColumnHeader('Name').scrollIntoView(colHeaderScrollOpts); + await gu.clickColumnMenuItem('Name', 'Rename column'); + await gu.getOpenEditingLabel(await gu.getColumnHeader('Name')).sendKeys('Renamed', $.ENTER); + await gu.waitForServer(); + await assert.isPresent(gu.getColumnHeader('Renamed'), true); + assert.deepEqual(await gu.getGridValues({ + cols: ['Renamed'], + rowNums: [3, 4, 5], + }), ['A Coruña (La Coruña)', 'Aachen', 'Aalborg']); + + // Verify that undo/redo works with renaming + await gu.undo(); + await assert.isPresent(gu.getColumnHeader('Name'), true); + assert.deepEqual(await gu.getGridValues({ + cols: ['Name'], + rowNums: [3, 4, 5], + }), ['A Coruña (La Coruña)', 'Aachen', 'Aalborg']); + await gu.redo(); + await assert.isPresent(gu.getColumnHeader('Renamed'), true); + assert.deepEqual(await gu.getGridValues({ + cols: ['Renamed'], + rowNums: [3, 4, 5], + }), ['A Coruña (La Coruña)', 'Aachen', 'Aalborg']); + + // Refresh the page and check that the rename sticks. + await driver.navigate().refresh(); + assert.equal(await $('.active_section .test-viewsection-title').wait().text(), 'CITY'); + await gu.waitForServer(5000); + await gu.clickCellRC(1, 4); + await assert.isPresent(gu.getColumnHeader('Renamed'), true); + + // Check that it is renamed in the sidepane + await gu.openSidePane('field'); + assert.equal(await $(".test-field-label").val(), "Renamed"); + + // Check that both the label and the Id are changed when label and Id are linked + let deriveIdCheckbox = $(".test-field-derive-id"); + assert.isTrue(await deriveIdCheckbox.is('[class*=-selected]')); + await deriveIdCheckbox.click(); + await gu.waitForServer(); + assert.isFalse(await deriveIdCheckbox.is('[class*=-selected]')); + assert(await $(".test-field-col-id").val(), "Renamed"); + + // Check that just the label is changed when label and Id are unlinked + await gu.getColumnHeader('Renamed').scrollIntoView(colHeaderScrollOpts); + await gu.clickColumnMenuItem('Renamed', 'Rename column'); + await gu.getOpenEditingLabel(await gu.getColumnHeader('Renamed')).wait().sendKeys('foo', $.ENTER); + await gu.waitForServer(); + await assert.isPresent(gu.getColumnHeader('foo'), true); + await assert.isPresent(gu.getOpenEditingLabel(gu.getColumnHeader('foo')), false); + assert.equal(await $(".test-field-label").val(), "foo"); + assert(await $(".test-field-col-id").val(), "Renamed"); + + // Saving an identical column label should still close the input. + await gu.getColumnHeader('foo').scrollIntoView(colHeaderScrollOpts); + await gu.clickColumnMenuItem('foo', 'Rename column'); + await gu.userActionsCollect(true); + await gu.getOpenEditingLabel(await gu.getColumnHeader('foo')).wait().sendKeys('foo', $.ENTER); + await gu.waitForServer(); + await gu.userActionsVerify([]); + await gu.userActionsCollect(false); + await assert.isPresent(gu.getOpenEditingLabel(gu.getColumnHeader('foo')), false); + await gu.waitAppFocus(true); + + // Bug T384: Should save the column name after cancelling a rename earlier. + await gu.getColumnHeader('A').scrollIntoView(colHeaderScrollOpts); + await gu.clickColumnMenuItem('A', 'Rename column'); + await gu.getOpenEditingLabel(await gu.getColumnHeader('A')).wait().sendKeys('C', $.ESCAPE); + await gu.waitForServer(); + await assert.isPresent(gu.getColumnHeader('A'), true); + await gu.clickColumnMenuItem('A', 'Rename column'); + await gu.getOpenEditingLabel(await gu.getColumnHeader('A')).sendKeys('C', $.TAB); + await gu.waitForServer(); + await assert.isPresent(gu.getColumnHeader('C'), true); + }); + + it("should allow renaming columns with a click", async function() { + // Go to a non-target column first + await gu.getColumnHeader('Population').scrollIntoView(colHeaderScrollOpts); + await gu.getColumnHeader('Population').click(); + // Now select the column of interest + await gu.getColumnHeader('foo').scrollIntoView(colHeaderScrollOpts); + await gu.getColumnHeader('foo').click(); + // And click one more time to rename + await gu.getColumnHeader('foo').click(); + await gu.getOpenEditingLabel(await gu.getColumnHeader('foo')).sendKeys('foot', $.ENTER); + await gu.waitForServer(); + await assert.isPresent(gu.getColumnHeader('foo'), false); + await assert.isPresent(gu.getColumnHeader('foot'), true); + // Click to rename back again + await gu.getColumnHeader('foot').click(); + await gu.getOpenEditingLabel(await gu.getColumnHeader('foot')).sendKeys('foo', $.ENTER); + await gu.waitForServer(); + await assert.isPresent(gu.getColumnHeader('foo'), true); + await assert.isPresent(gu.getColumnHeader('foot'), false); + }); + + it("should allow deleting multiple columns", async function() { + await gu.actions.selectTabView('Country'); + + // delete shortcut should delete all selected columns + await gu.selectGridArea([1, 0], [1, 1]); + await gu.sendKeys([$.ALT, '-']); + await gu.waitForServer(); + assert.deepEqual(await gu.getGridLabels('Country'), + ["Continent", "Region", "SurfaceArea", "IndepYear", "Population", "LifeExpectancy", + "GNP", "GNPOld", "LocalName", "GovernmentForm", "HeadOfState", "Capital", "Code2"]); + // Undo to restore changes + await gu.undo(1, 5000); + + // delete menu item should delete all selected columns + await gu.selectGridArea([1, 2], [1, 8]); + await gu.clickColumnMenuItem('SurfaceArea', 'Delete', true); + await gu.waitForServer(); + assert.deepEqual(await gu.getGridLabels('Country'), + ["Code", "Name", "GNPOld", "LocalName", "GovernmentForm", "HeadOfState", "Capital", "Code2"]); + // Undo to restore changes + await gu.undo(1, 5000); + + // the delete shortcut should delete all columns in a cell selection as well + await gu.clickCellRC(2, 5); + await gu.sendKeys([$.SHIFT, $.RIGHT, $.RIGHT]); + await gu.sendKeys([$.ALT, '-']); + await gu.waitForServer(); + assert.deepEqual(await gu.getGridLabels('Country'), + ["Code", "Name", "Continent", "Region", "SurfaceArea", "GNP", "GNPOld", "LocalName", + "GovernmentForm", "HeadOfState", "Capital", "Code2"]); + // Undo to restore changes + await gu.undo(1, 5000); + + // Nudge first few columns back into view if they've drifted out of it. + await gu.toggleSidePanel('right', 'close'); + await gu.sendKeys($.LEFT, $.LEFT, $.LEFT); + + // opening a column menu outside the selection should move the selection + await gu.clickCellRC(2, 2); + await gu.sendKeys([$.SHIFT, $.RIGHT]); + await gu.clickColumnMenuItem('IndepYear', 'Delete', true); + await gu.waitForServer(); + assert.deepEqual(await gu.getGridLabels('Country'), + ["Code", "Name", "Continent", "Region", "SurfaceArea", "Population", + "LifeExpectancy", "GNP", "GNPOld", "LocalName", "GovernmentForm", "HeadOfState", + "Capital", "Code2"]); + // Undo to restore changes + await gu.undo(); + }); + + it("should allow hiding multiple columns", async function() { + await gu.actions.selectTabView('Country'); + await gu.openSidePane('view'); + + // hide menu item should hide all selected columns + await gu.selectGridArea([1, 2], [1, 8]); + await gu.clickColumnMenuItem('SurfaceArea', 'Hide 7 columns', true); + await gu.waitForServer(); + assert.deepEqual(await gu.getGridLabels('Country'), + ["Code", "Name", "GNPOld", "LocalName", "GovernmentForm", "HeadOfState", "Capital", "Code2"]); + assert.deepEqual(await $('.test-vfc-visible-fields .kf_draggable_content').array().text(), + ["Code", "Name", "GNPOld", "LocalName", "GovernmentForm", "HeadOfState", "Capital", "Code2"]); + assert.deepEqual(await $('.test-vfc-hidden-fields .kf_draggable_content').array().text(), + ["Name", "Continent", "Region", "SurfaceArea", "IndepYear", "Population", + "LifeExpectancy", "GNP", "Self"]); + // Undo to restore changes + await gu.undo(1, 5000); + + assert.deepEqual(await gu.getGridLabels('Country'), + ["Code", "Name", "Continent", "Region", "SurfaceArea", "IndepYear", "Population", + "LifeExpectancy", "GNP", "GNPOld", "LocalName", "GovernmentForm", "HeadOfState", + "Capital", "Code2"]); + + }); +}); diff --git a/test/nbrowser/CustomWidgetsConfig.ts b/test/nbrowser/CustomWidgetsConfig.ts index 965dd303..43ae72fe 100644 --- a/test/nbrowser/CustomWidgetsConfig.ts +++ b/test/nbrowser/CustomWidgetsConfig.ts @@ -158,7 +158,7 @@ async function checkSortMenu(state: 'empty' | 'modified' | 'customized' | 'empty } describe('CustomWidgetsConfig', function () { - this.timeout(30000); // almost 20 second on dev machine. + this.timeout(60000); const cleanup = setupTestSuite(); let mainSession: gu.Session; gu.bigScreen(); diff --git a/test/nbrowser/Dates.ntest.js b/test/nbrowser/Dates.ntest.js new file mode 100644 index 00000000..6c013004 --- /dev/null +++ b/test/nbrowser/Dates.ntest.js @@ -0,0 +1,504 @@ +import { assert, driver } from 'mocha-webdriver'; +import { $, gu, test } from 'test/nbrowser/gristUtil-nbrowser'; + +describe('Dates.ntest', function() { + const cleanup = test.setupTestSuite(this); + let doc; + before(async function() { + await gu.supportOldTimeyTestCode(); + doc = await gu.useFixtureDoc(cleanup, "Hello.grist", true); + await gu.toggleSidePanel("left", "close"); + }); + + afterEach(function() { + return gu.checkForErrors(); + }); + + it('should allow correct datetime reformatting', async function() { + await gu.openSidePane('field'); + + var cell = await gu.getCellRC(0, 0); + + // Move to the first column + await cell.click(); + await gu.sendKeys('2008-01-10 9:20pm', $.ENTER); + + // Change type to 'DateTime' + await gu.setType('DateTime'); + await $('.test-tz-autocomplete').wait().click(); + await gu.sendKeys($.DELETE, 'UTC', $.ENTER); + await gu.waitForServer(); + assert.equal(await cell.text(), '2008-01-10 9:20pm'); + await $('.test-type-transform-apply').wait().click(); + await gu.waitForServer(); + + // Change timezone to 'America/Los_Angeles' and check that the date is correct + await $('.test-tz-autocomplete').wait().click(); + await gu.sendKeys('Los An', $.ENTER); + await gu.waitForServer(); + assert.equal(await $('.test-tz-autocomplete input').val(), 'America/Los_Angeles'); + assert.equal(await cell.text(), '2008-01-10 1:20pm'); + + // Change format and check that date is reformatted + await gu.dateFormat('MMMM Do, YYYY'); + await gu.timeFormat('HH:mm:ss z'); + assert.equal(await gu.getCellRC(0, 0).text(), 'January 10th, 2008 13:20:00 PST'); + + // Change to custom format and check that the date is reformatted + await gu.dateFormat('Custom'); + + await $('$Widget_dateCustomFormat .kf_text').click(); + await gu.sendKeys($.SELECT_ALL, 'dddd', $.ENTER); + await gu.timeFormat("Custom"); + await $('$Widget_timeCustomFormat .kf_text').click(); + await gu.sendKeys($.SELECT_ALL, 'Hmm', $.ENTER); + await gu.waitForServer(); + assert.equal(await cell.text(), 'Thursday 1320'); + }); + + it('should include a functioning datetime editor', async function() { + var cell = await gu.getCellRC(0, 0); + + // DateTime editor should open, separate date and time, and replace incomplete format + // with YYYY-MM-DD + await cell.click(); + await gu.sendKeys($.ENTER); + assert.equal(await $('.celleditor_text_editor').first().val(), '2008-01-10'); + + // Date should be changable by clicking the calendar dates + await $('.celleditor_text_editor').first().sendKeys($.DOWN); // Opens date picker even if window has no focus. + await $('.datepicker .day:contains(19)').wait().click(); + await gu.sendKeys($.ENTER); + assert.equal(await cell.text(), 'Saturday 1320'); + + // Date editor should convert Moment formats to datepicker safe formats + // Date editor should allow tabbing between date and time entry boxes + await gu.dateFormat('MMMM Do, YYYY'); + await gu.timeFormat('h:mma'); + await cell.click(); + await gu.sendKeys($.ENTER); + assert.deepEqual(await $('.celleditor_text_editor').array().val(), + ['January 19th, 2008', '1:20pm']); + await gu.sendKeys($.SELECT_ALL, 'February 20th, 2009', $.TAB, '8:15am', $.ENTER); + await gu.waitForServer(); + assert.equal(await cell.text(), 'February 20th, 2009 8:15am'); + + // DateTime editor should close and save value when the user clicks away + await cell.click(); + await gu.sendKeys($.ENTER, $.SELECT_ALL, $.DELETE); + await gu.getCellRC(0, 3).click(); // click away + await gu.waitForServer(); + // Since only the date value was removed, the cell should give AltText of the time value + assert.equal(await cell.text(), '8:15am'); + assert.hasClass(await cell.find('.field_clip'), 'invalid'); + + // DateTime editor should close and revert value when the user presses escape + await cell.click(); + await gu.sendKeys($.ENTER, 'April 2, 1993', $.ESCAPE); + assert.equal(await cell.text(), '8:15am'); + }); + + it('should allow correct date reformatting', async function() { + var cell = await gu.getCellRC(0, 1); + + // Move to the first column + await cell.click(); + await gu.sendKeys('2016-01-08', $.ENTER); + + // Change type to 'Date' + await gu.setType('Date'); + await $('.test-type-transform-apply').wait().click(); + await gu.waitForServer(); // Make sure type is set + + // Check that the date is correct + await $('$Widget_dateFormat').wait(); + assert.equal(await cell.text(), '2016-01-08'); + + // Change format and check that date is reformatted + await gu.dateFormat('MMMM Do, YYYY'); + await gu.waitForServer(); + assert.equal(await cell.text(), 'January 8th, 2016'); + + // Try another format + await gu.dateFormat('DD MMM YYYY'); + await gu.waitForServer(); + assert.equal(await cell.text(), '08 Jan 2016'); + + // Change to custom format and check that the date is reformatted + await gu.dateFormat('Custom'); + await $('$Widget_dateCustomFormat .kf_text').click(); + await gu.sendKeys($.SELECT_ALL, 'dddd', $.ENTER); + await gu.waitForServer(); + assert.equal(await cell.text(), 'Friday'); + }); + + it('should include a functioning date editor', async function() { + var cell = await gu.getCellRC(0, 1); + + // Date editor should open and replace incomplete format with YYYY-MM-DD + await cell.click(); + await gu.sendKeys($.ENTER); + assert.equal(await $('.celleditor_text_editor').val(), '2016-01-08'); + + // Date should be changable by clicking the calendar dates + await $('.celleditor_text_editor').sendKeys($.DOWN); // Opens date picker even if window has no focus. + await $('.datepicker .day:contains(19)').wait().click(); + await gu.sendKeys($.ENTER); + await gu.waitForServer(); + assert.equal(await cell.text(), 'Tuesday'); + + // Date editor should convert Moment formats to datepicker safe formats + // Date editor should save the date on enter press + await gu.dateFormat('MMMM Do, YYYY'); + await cell.click(); + await gu.sendKeys($.ENTER); + assert.equal(await $('.celleditor_text_editor').val(), 'January 19th, 2016'); + await gu.sendKeys($.SELECT_ALL, 'February 20th, 2016', $.ENTER); + await gu.waitForServer(); + assert.equal(await cell.text(), 'February 20th, 2016'); + + // Date editor should close and save value when the user clicks away + await cell.click(); + await gu.sendKeys($.ENTER, $.SELECT_ALL, $.DELETE); + await gu.getCellRC(0, 3).click(); // click away + await gu.waitForServer(); + assert.equal(await cell.text(), ''); + + // Date editor should close and revert value when the user presses escape + await cell.click(); + await gu.sendKeys($.ENTER, 'April 2, 1993', $.ESCAPE); + assert.equal(await cell.text(), ''); + }); + + it('should reload values correctly after reopen', async function() { + await gu.getCellRC(0, 0).click(); + await gu.sendKeys('February 20th, 2009', $.TAB, '8:15am', $.ENTER); + await gu.getCellRC(0, 1).click(); + await gu.sendKeys('January 19th, 1968', $.ENTER); + await gu.getCellRC(1, 1).click(); + await gu.sendKeys($.DELETE); + await gu.waitForServer(); + await gu.getCellRC(0, 2).click(); + await gu.waitAppFocus(true); + await gu.sendKeys('='); + await $('.test-editor-tooltip-convert').click(); + await gu.sendKeys('$A', $.ENTER); + await gu.waitForServer(); + await gu.waitAppFocus(true); + await gu.getCellRC(0, 3).click(); + await gu.sendKeys('='); + await gu.waitAppFocus(false); + await gu.sendKeys('$B', $.ENTER); + await gu.waitForServer(); + + assert.deepEqual(await gu.getGridValues({rowNums: [1, 2], cols: ['A', 'B', 'C', 'D']}), [ + 'February 20th, 2009 8:15am', + 'January 19th, 1968', + '2009-02-20 08:15:00-08:00', + '1968-01-19', + '', '', '', '' + ]); + + // We don't have a quick way to shutdown a document and reopen from scratch. So instead, we'll + // make a copy of the document, and open that to test that values got saved correctly. + // TODO: it would be good to add a way to reload document from scratch, perhaps by reloading + // with a special URL fragment. + await gu.copyDoc(doc.id, true); + + assert.deepEqual(await gu.getGridValues({rowNums: [1, 2], cols: ['A', 'B', 'C', 'D']}), [ + 'February 20th, 2009 8:15am', + 'January 19th, 1968', + '2009-02-20 08:15:00-08:00', + '1968-01-19', + '', '', '', '' + ]); + }); + + it('should support shortcuts to insert date/time', async function() { + await gu.openSidePane('field'); + // Check the types of the first two columns. + await gu.clickCellRC(0, 0); + await gu.assertType('DateTime'); + await gu.clickCellRC(0, 1); + await gu.assertType('Date'); + // Insert a few more columns: empty, Text, Numeric. + await addColumn(); + await addColumn(); + await addColumn(); + await gu.clickCellRC(0, 3); + await gu.setType('Numeric'); + await gu.clickCellRC(0, 4); + await gu.setType('Text'); + + // Override Date.now() and timezone in the current browser page to return a consistent value, + // used e.g. for the default for the year and month. + await driver.executeScript( + "Date.now = () => 1477548296087; " + // This value is 2016-10-27 02:04:56.087 EST + "exposeModulesForTests().then(() => { " + + "window.exposedModules.moment.tz.setDefault('America/New_York');" + + "});" + ); + + async function fillWithShortcuts() { + await gu.toggleSidePanel('right', 'close'); + + // Type the Date-only shortcut into each cell in the second row. + await gu.clickCellRC(1, 0); + for (var i = 0; i < 6; i++) { + await gu.sendKeys([$.MOD, ';'], $.TAB); + } + + // Type the Date-Time shortcut into each cell in the third row. + await gu.clickCellRC(2, 0); + for (i = 0; i < 6; i++) { + await gu.sendKeys([$.MOD, $.SHIFT, ';'], $.TAB); + } + } + + // Change document timezone to US/Hawaii (3 hours behind LA, which is TZ of the first column). + // We check that shortcuts for Text/Any columns use the document timezone. + await setGlobalTimezone('US/Hawaii'); + await fillWithShortcuts(); + // Compare the values. NOTE: this assumes EST timezone for the browser's local time. + assert.deepEqual(await gu.getGridValues({rowNums: [2, 3], cols: [0, 1, 2, 3, 4]}), [ + // Note that column A has Los_Angeles timezone set, so time differs from Hawaii. + // Note that Date column gets the date in Hawaii, not local or UTC (both 2016-10-27). + // The originally empty column had its type guessed as Date when the current date was first entered, + // hence "2016-10-26" appears in both rows. + "October 26th, 2016 11:04pm", "October 26th, 2016", "2016-10-26", "0", "2016-10-26", + "October 26th, 2016 11:04pm", "October 26th, 2016", "2016-10-26", "0", "2016-10-26 20:04:56", + ]); + + // Undo the 8 cells we actually filled in, and check that the empty column reverted to Any + await gu.undo(8); + await gu.clickCellRC(1, 2); + await gu.assertType('Any'); + + // Change document timezone back to America/New_York. + await setGlobalTimezone('America/New_York'); + await fillWithShortcuts(); + // Compare the values. NOTE: this assumes EST timezone for the browser's local time. + assert.deepEqual(await gu.getGridValues({rowNums: [2, 3], cols: [0, 1, 2, 3, 4]}), [ + // Note that column A has Los_Angeles timezone set, so date differs by one from New_York. + "October 26th, 2016 11:04pm", "October 27th, 2016", "2016-10-27", "0", "2016-10-27", + "October 26th, 2016 11:04pm", "October 27th, 2016", "2016-10-27", "0", "2016-10-27 02:04:56", + ]); + }); + + it('should allow navigating the datepicker with the keyboard', async function() { + // Change the date using the datepicker. + let cell = await gu.getCellRC(0, 1); + await cell.scrollIntoView({inline: "end"}).click(); + await gu.sendKeys($.ENTER); + await gu.waitAppFocus(false); + await gu.sendKeys($.UP, $.UP, $.LEFT, $.ENTER); + await gu.waitForServer(); + assert.equal(await cell.text(), 'January 11th, 1968'); + + // Do the same in the datetime editor. + cell = await gu.getCellRC(1, 0); + await cell.click(); + await gu.sendKeys($.ENTER); + await gu.waitAppFocus(false); + await gu.sendKeys($.UP, $.RIGHT, $.RIGHT, $.ENTER); + await gu.waitForServer(); + assert.equal(await cell.text(), 'October 28th, 2016 11:04pm'); + + // Start navigating the datepicker, then start typing to return to using the cell editor. + cell = await gu.getCellRC(1, 1); + await cell.click(); + // The first backspace should return to cell edit mode, then the following keys should + // change the year to 2009. + await gu.sendKeys($.ENTER); + await gu.waitAppFocus(false); + await gu.sendKeys($.DOWN, $.RIGHT, $.BACK_SPACE, '9', $.LEFT, $.BACK_SPACE, '0', $.ENTER); + await gu.waitForServer(); + assert.equal(await cell.text(), 'October 27th, 2009'); + }); + + // NOTE: This addresses a bug where typical date entry formats were not recognized. + // See https://phab.getgrist.com/T308 + it('should allow using common formats to enter the date', async function() { + let cell = await gu.getCellRC(2, 1); + await cell.click(); + await gu.sendKeys('April 2 1993', $.ENTER); + await gu.waitForServer(); + assert.equal(await cell.text(), 'April 2nd, 1993'); + + cell = await gu.getCellRC(1, 0); + await cell.click(); + await gu.sendKeys('December', $.ENTER); + await gu.waitForServer(); + assert.equal(await cell.text(), `December 1st, 2016 11:04pm`); + + cell = await gu.getCellRC(0, 1); + await cell.click(); + await gu.sendKeys('7-Sep', $.ENTER); + await gu.waitForServer(); + assert.equal(await cell.text(), `September 7th, 2016`); + + await cell.click(); + await gu.sendKeys('6/8', $.ENTER); + await gu.waitForServer(); + assert.equal(await cell.text(), `June 8th, 2016`); + + // The selected format should take precedence over the default format when + // parsing the date. Entering the same thing as before (6/8) will yield a different + // result after changing the format. + await gu.openSidePane('field'); + cell = await gu.getCellRC(1, 1); + await cell.click(); + await gu.dateFormat('DD-MM-YYYY'); + await cell.click(); + await gu.sendKeys('6/8', $.ENTER); + await gu.waitForServer(); + await gu.dateFormat('MMMM Do, YYYY'); + assert.equal(await cell.text(), `August 6th, 2016`); + + cell = await gu.getCellRC(2, 1); + await cell.click(); + await gu.sendKeys('1937', $.ENTER); + await gu.waitForServer(); + assert.equal(await cell.text(), `January 1st, 1937`); + }); + + it('should not attempt to parse non-dates', async function() { + // Should allow AltText + let cell = await gu.getCellRC(2, 1); + await cell.click(); + await gu.sendKeys('Applesauce', $.ENTER); + await gu.waitForServer(); + assert.equal(await cell.text(), 'Applesauce'); + await assert.hasClass(cell.find('.field_clip'), 'invalid'); + // Should allow AltText even of numbers that cannot be parsed as dates. + // Manually entered numbers should not be read as timestamps. + cell = await gu.getCellRC(1, 0); + await cell.click(); + await gu.sendKeys('100000', $.ENTER); + await gu.waitForServer(); + assert.equal(await cell.text(), '100000 11:04pm'); + await assert.hasClass(cell.find('.field_clip'), 'invalid'); + // Should give AltText if just the time is entered but not the date. + cell = await gu.getCellRC(1, 0); + await cell.click(); + await gu.sendKeys($.ENTER, $.TAB, '3', $.ENTER); + await gu.waitForServer(); + assert.equal(await cell.text(), '100000 11:04pm 3'); + await assert.hasClass(cell.find('.field_clip'), 'invalid'); + }); + + it("should allow working with naive date object", async function() { + await gu.clickCellRC(0, 1); + await gu.sendKeys([$.ALT, '=']); + await gu.waitForServer(); + await gu.sendKeys("Diff", $.ENTER); + await gu.waitForServer(); + await gu.sendKeys('='); + await gu.waitAppFocus(false); + await gu.sendKeys('($A-DTIME($B)).total_seconds()', $.ENTER); + await gu.waitForServer(); + await gu.waitAppFocus(); + assert.deepEqual(await gu.getCellRC(0, 2).text(), '-230211900'); + + // change global timezone should recompute formula + await setGlobalTimezone('Paris'); + assert.deepEqual(await gu.getCellRC(0, 2).text(), '-230190300'); + }); + + // NOTE: This tests a specific bug where AltText values in a column that has been coverted + // to a date column do not respond to updates until refresh. This bug was exposed via the + // error dom in FieldBuilder not being re-evaluated after a column transform. + it('should allow deleting AltText values in a newly changed Date column', async function() { + // Change the type to text and enter a text value. + await gu.clickCellRC(0, 1); + await gu.setType('Text'); + await gu.applyTypeConversion(); + await gu.clickCellRC(2, 1); + await gu.sendKeys('banana', $.ENTER); + await gu.waitForServer(); + assert.equal(await gu.getCellRC(2, 1).text(), 'banana'); + + // Change back to Date and try to remove the text. + await gu.setType('Date'); + await $('.test-type-transform-apply').wait().click(); + await gu.waitForServer(); + assert.equal(await gu.getCellRC(2, 1).text(), 'banana'); + await gu.clickCellRC(2, 1); + await gu.sendKeys($.BACK_SPACE); + await gu.waitForServer(); + assert.equal(await gu.getCellRC(2, 1).text(), ''); + await gu.undo(); + }); + + it("should report informative error when AltText is used for date", async function() { + // Enter a formula column that uses a date. + await gu.clickCellRC(0, 1); + await gu.sendKeys([$.ALT, '=']); + await gu.waitForServer(); + await gu.sendKeys("Month", $.ENTER); + await gu.waitForServer(); + await gu.sendKeys("=$B.month", $.ENTER); + await gu.waitForServer(); + + assert.deepEqual(await gu.getGridValues({rowNums: [1, 2, 3, 4], cols: ['B', 'Month']}), [ + "June 8th, 2016", "6", + "August 6th, 2016", "8", + "banana", "#Invalid Date: banana", + "", "#AttributeError", + ]); + }); + + it('should default timezone to document\'s timezone', async function() { + // add a DateTime column + await addDateTimeColumn(); + await gu.timeFormat('HH:mm:ss'); + // BUG: it is required to click somewhere after setting the type of a column for the shortcut to + // work + // TODO: removes gu.getCellRC(1, 3).click() below when its fixed + await gu.getCellRC(1, 3).click(); + // get the current date + await gu.sendKeys([$.MOD, $.SHIFT, ';']); + await gu.waitForServer(); + const date1 = await gu.getCellRC(1, 3).text(); + // check default timezone + assert.equal(await $('.test-tz-autocomplete input').val(), 'Europe/Paris'); + // set global document timezone to 'Europe/Paris' + await setGlobalTimezone('America/Los_Angeles'); + // add another DateTime column + await addDateTimeColumn(); + await gu.timeFormat('HH:mm:ss'); + // todo: same as for gu.getCellRC(1, 3).click(); + await gu.getCellRC(1, 4).click(); + // get the current date + await gu.sendKeys([$.MOD, $.SHIFT, ';']); + await gu.waitForServer(); + const date2 = await gu.getCellRC(1, 4).text(); + // check default timezone + assert.equal(await $('.test-tz-autocomplete input').val(), 'America/Los_Angeles'); + // check that the delta between date1 and date2 is coherent with the delta between + // 'Europe/Paris' and 'America/Los_Angeles' timezones. + const delta = (new Date(date1) - new Date(date2)) / 1000 / 60 / 60; + assert.isAbove(delta, 6); + assert.isBelow(delta, 12); + }); +}); + +async function addDateTimeColumn() { + await addColumn(); + return gu.setType('DateTime'); +} + +async function addColumn() { + await gu.sendKeys([$.ALT, '=']); + await gu.waitForServer(); + return gu.sendKeys($.ESCAPE); +} + +async function setGlobalTimezone(name) { + await $('.test-user-icon').click(); // open the user menu + await $('.test-dm-doc-settings').click(); + await $('.test-tz-autocomplete').click(); + await $(`.test-acselect-dropdown li:contains(${name})`).click(); + await gu.waitForServer(); + await driver.navigate().back(); +} diff --git a/test/nbrowser/DetailView.ntest.js b/test/nbrowser/DetailView.ntest.js new file mode 100644 index 00000000..9c3a00ec --- /dev/null +++ b/test/nbrowser/DetailView.ntest.js @@ -0,0 +1,118 @@ +import { assert } from 'mocha-webdriver'; +import { $, gu, test } from 'test/nbrowser/gristUtil-nbrowser'; + +describe("DetailView.ntest", function () { + const cleanup = test.setupTestSuite(this); + gu.bigScreen(); + + before(async function() { + await gu.supportOldTimeyTestCode(); + await gu.useFixtureDoc(cleanup, "Favorite_Films.grist", true); + + // Open the view tab. + await gu.openSidePane('view'); + + // Open the 'All' view + await gu.actions.selectTabView('All'); + + // close the side pane + await gu.toggleSidePanel('left', 'close'); + }); + + afterEach(function() { + return gu.checkForErrors(); + }); + + it('should allow switching between card and detail view', async function() { + await gu.actions.viewSection('Performances detail').selectSection(); + + // Check that the detail cells have the correct values. + assert.deepEqual(await gu.getVisibleDetailCells('Actor', [1]), ['Tom Hanks']); + await $('.grist-single-record__menu .detail-right').click(); + // rowNum is always 1 for detail cells now. + assert.deepEqual(await gu.getVisibleDetailCells('Actor', [1]), ['Tim Allen']); + + // Swap to Card List view, check values. + await $('.test-right-panel button:contains(Change Widget)').click(); + await $('.test-wselect-type:contains(Card List)').click(); + await $('.test-wselect-addBtn').click(); + await gu.waitForServer(); + assert.deepEqual(await gu.getVisibleDetailCells('Actor', [1, 2]), + ['Tom Hanks', 'Tim Allen']); + + // Swap back to Card view, re-check values. + await $('.test-right-panel button:contains(Change Widget)').click(); + await $('.test-wselect-type:contains(Card)').click(); + await $('.test-wselect-addBtn').click(); + await gu.waitForServer(); + assert.deepEqual(await gu.getVisibleDetailCells('Actor', [1]), ['Tim Allen']); + await $('.grist-single-record__menu .detail-left').click(); + assert.deepEqual(await gu.getVisibleDetailCells('Actor', [1]), ['Tom Hanks']); + }); + + it('should allow editing cells', async function() { + // Updates should be reflected in the detail floating rowModel cell. + await gu.sendKeys('Roger Federer', $.ENTER); + await gu.waitForServer(); + assert.deepEqual(await gu.getVisibleDetailCells('Actor', [1]), ['Roger Federer']); + + // Undo updates should be reflected as well. + await gu.sendKeys([$.MOD, 'z']); + await gu.waitForServer(); + assert.deepEqual(await gu.getVisibleDetailCells('Actor', [1]), ['Tom Hanks']); + }); + + // Note: This is a test of a specific bug related to the detail rowModel being resized after + // being unset. + it('should allow row resize operations after switching section type', async function() { + // Switch to Card List view and enter a formula. This should cause the scrolly to resize all rows. + // If the detail view rowModel is wrongly resized, the action will fail. + await $('.test-right-panel button:contains(Change Widget)').click(); + await $('.test-wselect-type:contains(Card List)').click(); + await $('.test-wselect-addBtn').click(); + await gu.waitForServer(); + await gu.sendKeys('='); + await $('.test-editor-tooltip-convert').click(); // Convert to a formula + await gu.sendKeys('100', $.ENTER); + await gu.waitForServer(); + assert.deepEqual(await gu.getVisibleDetailCells('Actor', [1, 2, 3, 4]), + ['100', '100', '100', '100']); + }); + + 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(); + assert.deepEqual(await gu.getVisibleDetailCells('Film', [14]), ['Jurassic Park']); + // Check that adding 'Star Wars' to the detail view add record row adds it as a row. + await $('.test-right-panel button:contains(Change Widget)').click(); + await $('.test-wselect-type:contains(Card)').click(); + await $('.test-wselect-addBtn').click(); + await gu.waitForServer(); + await $('.detail-add-btn').wait().click(); + // Card view, so rowNum is now 1 + await gu.getDetailCell('Film', 1).click(); + await gu.sendKeys('Star Wars', $.ENTER); + await gu.waitForServer(); + assert.deepEqual(await gu.getVisibleDetailCells('Film', [1]), ['Star Wars']); + + // 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 gu.waitForServer(); + assert.deepEqual(await gu.getDetailCell('Character', 1).text(), '100'); + + // Should not throw errors when deleting the add record row. + await $('.detail-add-btn').click(); + await gu.sendKeys([$.MOD, $.DELETE]); + // Errors will be detected in afterEach. + }); +}); diff --git a/test/nbrowser/DocTutorial.ts b/test/nbrowser/DocTutorial.ts index 806ef994..86d5782d 100644 --- a/test/nbrowser/DocTutorial.ts +++ b/test/nbrowser/DocTutorial.ts @@ -6,7 +6,7 @@ import {server, setupTestSuite} from 'test/nbrowser/testUtils'; import {EnvironmentSnapshot} from 'test/server/testUtils'; describe('DocTutorial', function () { - this.timeout(30000); + this.timeout(60000); setupTestSuite(); gu.bigScreen(); @@ -578,7 +578,7 @@ describe('DocTutorial', function () { // Check that the changes we made earlier are included. assert.equal( - await driver.find('.test-doc-tutorial-popup p').getText(), + await driver.findWait('.test-doc-tutorial-popup p', 2000).getText(), 'Welcome to the Grist Basics tutorial V2.' ); }); diff --git a/test/nbrowser/Export.ntest.js b/test/nbrowser/Export.ntest.js new file mode 100644 index 00000000..a61c65fc --- /dev/null +++ b/test/nbrowser/Export.ntest.js @@ -0,0 +1,61 @@ +import { assert } from 'mocha-webdriver'; +import { $, gu, test } from 'test/nbrowser/gristUtil-nbrowser'; + +const fse = require('fs-extra'); +const path = require('path'); +const axios = require('axios'); + +// Authentication headers to include into axios requests. +const headers = {Authorization: 'Bearer api_key_for_userz'}; + +describe('Export.ntest', function() { + const cleanup = test.setupTestSuite(this); + const pathsExpected = { + base: path.resolve(gu.fixturesRoot, "export-csv", "CCTransactions.csv"), + sorted: path.resolve(gu.fixturesRoot, "export-csv", "CCTransactions-DBA-desc.csv") + }; + let dataExpected = {}; + + before(async function() { + await gu.supportOldTimeyTestCode(); + await gu.useFixtureDoc(cleanup, "CCTransactions.grist", true); + + // Read the expected contents before the test case starts, to simplify the promises there. + // (don't really need that simplification any more though). + for (const [key, fname] of Object.entries(pathsExpected)) { + dataExpected[key] = await fse.readFile(fname, {encoding: 'utf8'}); + } + }); + + afterEach(function() { + return gu.checkForErrors(); + }); + + it('should export correct data', async function() { + await $('.test-tb-share').click(); + // Once the menu opens, get the href of the link. + await $('.grist-floating-menu').wait(); + const href = await $('.grist-floating-menu a:contains(CSV)').wait().getAttribute('href'); + // Download the data at the link and compare to expected. + const resp = await axios.get(href, {responseType: 'text', headers}); + assert.equal(resp.headers['content-disposition'], + 'attachment; filename="CCTransactions.csv"'); + assert.equal(resp.data, dataExpected.base); + await $('.test-tb-share').click(); + }); + + it('should respect active sort', async function() { + await gu.openColumnMenu('Doing Business As'); + await $('.grist-floating-menu .test-sort-dsc').click() + await $('.test-tb-share').click(); + // Once the menu opens, get the href of the link. + await $('.grist-floating-menu').wait(); + const href = await $('.grist-floating-menu a:contains(CSV)').wait().getAttribute('href'); + // Download the data at the link and compare to expected. + const resp = await axios.get(href, {responseType: 'text', headers}); + assert.equal(resp.data, dataExpected.sorted); + }); + + // TODO: We should have a test case with multiple sections on the screen, that checks that + // export runs for the currently selected section. +}); diff --git a/test/nbrowser/FieldConfigTab.ntest.js b/test/nbrowser/FieldConfigTab.ntest.js new file mode 100644 index 00000000..756e1045 --- /dev/null +++ b/test/nbrowser/FieldConfigTab.ntest.js @@ -0,0 +1,195 @@ +import { assert } from 'mocha-webdriver'; +import { $, gu, test } from 'test/nbrowser/gristUtil-nbrowser'; + +describe('FieldConfigTab.ntest', function() { + const cleanup = test.setupTestSuite(this); + before(async function() { + await gu.supportOldTimeyTestCode(); + await gu.useFixtureDoc(cleanup, "Hello.grist", true); + }); + + afterEach(function() { + return gu.checkForErrors(); + }); + + it("should stay open when switching between columns or views", async function() { + // Add another table to the document. + await gu.actions.addNewTable(); + + await gu.actions.selectTabView('Table1'); + await gu.openSidePane('field'); + var fieldLabel = await $(".test-field-label").wait(assert.isDisplayed).elem(); + + await assert.isDisplayed(fieldLabel); + assert.equal(await fieldLabel.val(), "A"); + + // Move cursor to a different column. + await $("$GridView_columnLabel:nth-child(2)").click(); + assert.equal(await fieldLabel.val(), "B"); + + // Switch to another view. The first column should be selected. + await gu.actions.selectTabView('Table2'); + + fieldLabel = $(".test-field-label").elem(); + await assert.isDisplayed(fieldLabel); + assert.equal(await fieldLabel.val(), "A"); + }); + + it("should support changing the column label and id together", async function() { + await gu.actions.selectTabView('Table1'); + var fieldLabel = await $(".test-field-label").elem(); + await gu.clickCellRC(0, 0); // Move back to the first cell. + assert.equal(await fieldLabel.val(), "A"); + await $(".test-field-label").sendNewText("foo"); + await gu.waitForServer(); + + // Check that both the label and colId changed in the side pane. + assert.equal(await fieldLabel.val(), "foo"); + await $(".test-field-col-id").wait(async function(el) { return assert.equal(await el.val(), "$foo"); }); + + // Check that the label changed among column headers. + assert.equal(await $("$GridView_columnLabel:nth-child(1)").text(), "foo"); + }); + + it("should support changing the column label and id separately", async function() { + await gu.actions.selectTabView('Table1'); + await $("$GridView_columnLabel:nth-child(2)").click(); + var fieldLabel = $(".test-field-label"); + assert.equal(await fieldLabel.val(), "B"); + + // Uncheck the "derive id" checkbox. + var deriveIdCheckbox = $(".test-field-derive-id"); + assert.isTrue(await deriveIdCheckbox.is('[class*=-selected]')); + await deriveIdCheckbox.click(); + await gu.waitForServer(); + assert.isFalse(await deriveIdCheckbox.is('[class*=-selected]')); + + // Check that only the label changed in the side pane. + await fieldLabel.sendNewText("bar"); + await gu.waitForServer(); + assert.equal(await fieldLabel.val(), "bar"); + await $("$GridView_columnLabel:nth-child(2)").wait(async function(el) { return assert.equal(await el.text(), "bar"); }); + + // Id should be unchanged, but we should be able to change it now. + assert.deepEqual(await gu.getGridValues({ rowNums: [1, 2, 3, 4], cols: [1] }), + ['', 'world', '', '']); + assert(await $(".test-field-col-id").val(), "B"); + await $(".test-field-col-id").sendNewText("baz"); + assert(await $(".test-field-col-id").val(), "baz"); + assert.equal(await fieldLabel.val(), "bar"); + assert.equal(await $("$GridView_columnLabel:nth-child(1)").text(), "foo"); + assert.equal(await $("$GridView_columnLabel:nth-child(2)").text(), "bar"); + + // Make sure the changing Ids does not effect the data in the column + assert.deepEqual(await gu.getGridValues({ rowNums: [1, 2, 3, 4], cols: [1] }), + ['', 'world', '', '']); + await assert.hasClass(gu.getCell(0, 1).find('.field_clip'), 'invalid', false); + }); + + describe('Duplicate Labels', async function() { + let fieldLabel, deriveIdCheckbox; + + beforeEach(() => { + fieldLabel = $(".test-field-label"); + deriveIdCheckbox = $(".test-field-derive-id"); + }); + + it('should allow duplicate labels with underived colIds', async function() { + // Change column 4 to have the same label as column 1 + await $("$GridView_columnLabel:nth-child(4)").click(); + assert.equal(await fieldLabel.val(), "D"); + assert.isTrue(await deriveIdCheckbox.is('[class*=-selected]')); + await deriveIdCheckbox.click(); + await gu.waitForServer(); + assert.isFalse(await deriveIdCheckbox.is('[class*=-selected]')); + await fieldLabel.sendNewText("foo"); + // Columns 1 and 4 should both be named foo + await $("$GridView_columnLabel:nth-child(1)").wait(async function(el) { return assert.equal(await el.text(), "foo"); }); + await $("$GridView_columnLabel:nth-child(4)").wait(async function(el) { return assert.equal(await el.text(), "foo"); }); + // But colId should be unchanged + assert(await $(".test-field-col-id").val(), "D"); + }); + + it('should allow duplicate labels with derived colIds', async function() { + // Now clicking the derive box should be leave the labels the same + // but the conflicting Id should be sanitized + await deriveIdCheckbox.click(); + await gu.waitForServer(); + assert.isTrue(await deriveIdCheckbox.is('[class*=-selected]')); + await deriveIdCheckbox.click(); + await gu.waitForServer(); + assert.isFalse(await deriveIdCheckbox.is('[class*=-selected]')); + await $("$GridView_columnLabel:nth-child(1)").scrollIntoView({inline: "end"}).click(); + await $("$GridView_columnLabel:nth-child(1)").wait(async function(el) { return assert.equal(await el.text(), "foo"); }); + assert(await $(".test-field-col-id").val(), "foo"); + await $("$GridView_columnLabel:nth-child(4)").click(); + await $("$GridView_columnLabel:nth-child(4)").wait(async function(el) { return assert.equal(await el.text(), "foo"); }); + assert(await $(".test-field-col-id").val(), "foo2"); + }); + + it('should not change the derived id unnecessarly', async function() { + // Toggling the box should not change the derived Id + await deriveIdCheckbox.click(); + await gu.waitForServer(); + assert.isTrue(await deriveIdCheckbox.is('[class*=-selected]')); + await deriveIdCheckbox.click(); + await gu.waitForServer(); + assert.isFalse(await deriveIdCheckbox.is('[class*=-selected]')); + assert(await $(".test-field-col-id").val(), "foo2"); + }); + + it('should not automatically modify the derived checkbox', async function() { + // When derived labels are changed to an existing Id, the derived box should remain checked + // even if the id and label are different + await $("$GridView_columnLabel:nth-child(1)").scrollIntoView({inline: "end"}).click(); + await $("$GridView_columnLabel:nth-child(1)").wait(async function(el) { return assert.equal(await el.text(), "foo"); }); + assert(await $(".test-field-col-id").val(), "foo"); + assert.isTrue(await deriveIdCheckbox.is('[class*=-selected]')); + await fieldLabel.sendNewText("foo2"); + await gu.waitForServer(); + assert.equal(await fieldLabel.val(), "foo2"); + assert(await $(".test-field-col-id").val(), "foo2_2"); + assert.isTrue(await deriveIdCheckbox.is('[class*=-selected]')); + }); + + it('should allow out of sync colIds to still derive from labels', async function() { + // Entering a new label should still sync the Id + await fieldLabel.sendNewText("foobar"); + await gu.waitForServer(); + assert.isTrue(await deriveIdCheckbox.is('[class*=-selected]')); + await deriveIdCheckbox.click(); + assert(await $(".test-field-col-id").val(), "foobar"); + }); + }); + + it("should allow editing column data after column rename", async function() { + await gu.actions.selectTabView('Table1'); + await $("$GridView_columnLabel:nth-child(3)").click(); + assert.equal(await $(".test-field-label").val(), "C"); + + // Switch type to numeric. This makes it easier to tell whether the value actually gets + // processed by the server. + await gu.setType('Numeric'); + await $('.test-type-transform-apply').wait().click(); + await gu.waitForServer(); + var cell = await gu.getCellRC(0, 2); + await cell.click(); // row index 0, column index 2 + await gu.sendKeys('17', $.ENTER); + await gu.waitForServer(); + assert.equal(await cell.text(), '17'); + await assert.hasClass(cell.find('.field_clip'), 'invalid', false); + + // Rename the column, make sure we can still type into it, and get results from the server. + await $(".test-field-label").sendNewText("c2"); + await gu.waitForServer(); + assert.equal(await $("$GridView_columnLabel:nth-child(3)").text(), "c2"); + await gu.waitForServer(); + cell = await gu.getCellRC(0, 2); + await cell.click(); // row index 0, column index 2 + await gu.sendKeys('23', $.ENTER); + await gu.waitForServer(); + assert.equal(await cell.text(), '23'); + await assert.hasClass(cell.find('.field_clip'), 'invalid', false); + }); + +}); diff --git a/test/nbrowser/FieldSettings.ntest.js b/test/nbrowser/FieldSettings.ntest.js new file mode 100644 index 00000000..f5332d17 --- /dev/null +++ b/test/nbrowser/FieldSettings.ntest.js @@ -0,0 +1,324 @@ +/** + * When a field is present in multiple views, the different copies of it may use common or + * separate settings. This test verifies these behaviors and switching between them. + */ + +import { assert } from 'mocha-webdriver'; +import { $, gu, test } from 'test/nbrowser/gristUtil-nbrowser'; + +describe('FieldSettings.ntest', function() { + const cleanup = test.setupTestSuite(this); + + before(async function() { + await gu.supportOldTimeyTestCode(); + await gu.useFixtureDoc(cleanup, "FieldSettings.grist", true); + + await gu.actions.selectTabView("Rates"); + await gu.waitForServer(); + await gu.openSidePane('field'); + }); + + afterEach(async function() { + await gu.userActionsCollect(false); + return gu.checkForErrors(); + }); + + async function checkSections(position, settingsFunc, expectedBySection) { + await gu.waitForServer(); + for (let sectionName in expectedBySection) { + let [cellText, settingsValue] = expectedBySection[sectionName]; + const cell = await gu.getCell(Object.assign({section: sectionName}, position)); + await gu.clickCell(cell); + assert.equal(await cell.text(), cellText); + assert.equal(await settingsFunc(), settingsValue); + } + } + + + it('should respect common settings for regular options', async function() { + await gu.userActionsCollect(true); + + // Sections 'A' and 'B' use common settings, and 'C' uses separate. + // Check that changing the setting in A affects B, but does not affect C. + await gu.clickCell({section: 'A', rowNum: 1, col: 1}); + assert.equal(await gu.dateFormat(), 'YYYY-MM-DD'); + await gu.dateFormat('MM/DD/YYYY'); + await checkSections({rowNum: 1, col: 1}, () => gu.dateFormat(), { + A: ['01/02/2012', 'MM/DD/YYYY'], + B: ['01/02/2012', 'MM/DD/YYYY'], + C: ['2012-01-02', 'YYYY-MM-DD'], + }); + + // Check that changing C does not affect A or B. + await gu.dateFormat('MMMM Do, YYYY'); + await checkSections({rowNum: 1, col: 1}, () => gu.dateFormat(), { + A: ['01/02/2012', 'MM/DD/YYYY'], + B: ['01/02/2012', 'MM/DD/YYYY'], + C: ['January 2nd, 2012', 'MMMM Do, YYYY'], + }); + + // Verify actions emitted. These are obtained from pasting the output, but the important thing + // about them is that it's one action for each change, one for the table, one for the field. + await gu.userActionsVerify([ + ["UpdateRecord", "_grist_Tables_column", 15, {"widgetOptions": + '{"widget":"TextBox","dateFormat":"MM/DD/YYYY","isCustomDateFormat":false,"alignment":"left"}'}], + ["UpdateRecord", "_grist_Views_section_field", 145, {"widgetOptions": + '{"widget":"TextBox","dateFormat":"MMMM Do, YYYY","isCustomDateFormat":false,"alignment":"left"}'}], + ]); + + // Undo, checking that the 2 actions only require 2 undos, and verify. + await gu.undo(2); + await checkSections({rowNum: 1, col: 1}, () => gu.dateFormat(), { + A: ['2012-01-02', 'YYYY-MM-DD'], + B: ['2012-01-02', 'YYYY-MM-DD'], + C: ['2012-01-02', 'YYYY-MM-DD'], + }); + }); + + + it('should respect common settings for visibleCol', async function() { + // Same as above but for changing "visibleCol", which involves extra actions to update the + // display helper column. + await gu.userActionsCollect(true); + await gu.clickCell({section: 'A', rowNum: 1, col: 0}); + assert.equal(await $('.test-fbuilder-ref-col-select .test-select-row').text(), 'Full Name'); + await gu.setVisibleCol('Last Name'); + await checkSections({rowNum: 2, col: 0}, () => $('.test-fbuilder-ref-col-select .test-select-row').text(), { + A: ['Klein', 'Last Name'], + B: ['Klein', 'Last Name'], + C: ['Klein, Cordelia', 'Full Name'], + }); + await gu.userActionsVerify([ + ["UpdateRecord", "_grist_Tables_column", 12, {"visibleCol":3}], + ["SetDisplayFormula", "Rates", null, 12, "$Person.Last_Name"], + ]); + + await gu.clickCell({section: 'C', rowNum: 1, col: 0}); + await gu.setVisibleCol('First Name'); + await checkSections({rowNum: 2, col: 0}, () => $('.test-fbuilder-ref-col-select .test-select-row').text(), { + A: ['Klein', 'Last Name'], + B: ['Klein', 'Last Name'], + C: ['Cordelia', 'First Name'], + }); + await gu.userActionsVerify([ + ["UpdateRecord", "_grist_Views_section_field", 141, {"visibleCol":2}], + ["SetDisplayFormula", "Rates", 141, null, "$Person.First_Name"], + ]); + + // Same for changing "visibleCol" to the special "RowID" value. + await gu.clickCell({section: 'A', rowNum: 1, col: 0}); + await gu.setVisibleCol('Row ID'); + await checkSections({rowNum: 2, col: 0}, () => $('.test-fbuilder-ref-col-select .test-select-row').text(), { + A: ['People[14]', 'Row ID'], + B: ['People[14]', 'Row ID'], + C: ['Cordelia', 'First Name'], + }); + await gu.userActionsVerify([ + ["UpdateRecord", "_grist_Tables_column", 12, {"visibleCol":0}], + ["SetDisplayFormula", "Rates", null, 12, ""], + ]); + + // Undo here so we can verify that per-field "Row ID" choice overrides per-column choice. + await gu.undo(); + + await gu.userActionsCollect(true); + await gu.clickCell({section: 'C', rowNum: 1, col: 0}); + await gu.setVisibleCol('Row ID'); + await checkSections({rowNum: 2, col: 0}, () => $('.test-fbuilder-ref-col-select .test-select-row').text(), { + A: ['Klein', 'Last Name'], + B: ['Klein', 'Last Name'], + C: ['People[14]', 'Row ID'], + }); + + // Verify actions emitted. + await gu.userActionsVerify([ + ["UpdateRecord", "_grist_Views_section_field", 141, {"visibleCol":0}], + ["SetDisplayFormula", "Rates", 141, null, ""], + ]); + + // Undo; we made 4 actions, but already ran one undo earlier. + await gu.undo(3); + await checkSections({rowNum: 2, col: 0}, () => $('.test-fbuilder-ref-col-select .test-select-row').text(), { + A: ['Klein, Cordelia', 'Full Name'], + B: ['Klein, Cordelia', 'Full Name'], + C: ['Klein, Cordelia', 'Full Name'], + }); + }); + + + it('should allow switching to separate settings', async function() { + // Switch B to use separate settings. + await gu.userActionsCollect(true); + await gu.clickCell({section: 'B', rowNum: 1, col: 1}); + await gu.fieldSettingsUseSeparate(); + + await gu.userActionsVerify([ + ["UpdateRecord", "_grist_Views_section_field", 140, {"widgetOptions": + '{"widget":"TextBox","dateFormat":"YYYY-MM-DD","isCustomDateFormat":false,"alignment":"left"}'}], + ]); + + // Verify that options are preserved. + await checkSections({rowNum: 1, col: 1}, () => gu.dateFormat(), { + A: ['2012-01-02', 'YYYY-MM-DD'], + B: ['2012-01-02', 'YYYY-MM-DD'], + C: ['2012-01-02', 'YYYY-MM-DD'], + }); + + // Change option in B and see that A and C are not affected. + await gu.clickCell({section: 'B', rowNum: 1, col: 1}); + await gu.dateFormat('MM/DD/YYYY'); + await checkSections({rowNum: 1, col: 1}, () => gu.dateFormat(), { + A: ['2012-01-02', 'YYYY-MM-DD'], + B: ['01/02/2012', 'MM/DD/YYYY'], + C: ['2012-01-02', 'YYYY-MM-DD'], + }); + + // Change option in A and see that B is not affected. + await gu.clickCell({section: 'A', rowNum: 1, col: 1}); + await gu.dateFormat('MMMM Do, YYYY'); + await checkSections({rowNum: 1, col: 1}, () => gu.dateFormat(), { + A: ['January 2nd, 2012', 'MMMM Do, YYYY'], + B: ['01/02/2012', 'MM/DD/YYYY'], + C: ['2012-01-02', 'YYYY-MM-DD'], + }); + + await gu.undo(3); + }); + + + it('should allow switching to separate settings for visibleCol', async function() { + // Same as above for changing 'visibleCol' option; after separating, try changing B, then A. + await gu.userActionsCollect(true); + await gu.clickCell({section: 'B', rowNum: 2, col: 0}); + await gu.fieldSettingsUseSeparate(); + await gu.userActionsVerify([ + ["UpdateRecord", "_grist_Views_section_field", 136, {"widgetOptions":'{"widget":"Reference"}'}], + ["UpdateRecord", "_grist_Views_section_field", 136, {"visibleCol":4}], + ["SetDisplayFormula", "Rates", 136, null, "$Person.Full_Name"], + ]); + + await gu.setVisibleCol('First Name'); + await gu.clickCell({section: 'A', rowNum: 2, col: 0}); + await gu.setVisibleCol('Last Name'); + await checkSections({rowNum: 2, col: 0}, () => $('.test-fbuilder-ref-col-select .test-select-row').text(), { + A: ['Klein', 'Last Name'], + B: ['Cordelia', 'First Name'], + C: ['Klein, Cordelia', 'Full Name'], + }); + await gu.undo(3); + }); + + + it('should allow reverting to common settings', async function() { + // Change column in C to use different settings from A. + await gu.clickCell({section: 'C', rowNum: 1, col: 1}); + await gu.dateFormat('MMMM Do, YYYY'); + await checkSections({rowNum: 1, col: 1}, () => gu.dateFormat(), { + A: ['2012-01-02', 'YYYY-MM-DD'], + B: ['2012-01-02', 'YYYY-MM-DD'], + C: ['January 2nd, 2012', 'MMMM Do, YYYY'], + }); + + // Revert C to use common settings. Check that it matches A. + await gu.userActionsCollect(true); + await gu.fieldSettingsRevertToCommon(); + await checkSections({rowNum: 1, col: 1}, () => gu.dateFormat(), { + A: ['2012-01-02', 'YYYY-MM-DD'], + B: ['2012-01-02', 'YYYY-MM-DD'], + C: ['2012-01-02', 'YYYY-MM-DD'], + }); + await gu.userActionsVerify([ + ["UpdateRecord", "_grist_Views_section_field", 145, {"widgetOptions":""}], + ]); + await gu.undo(2); + }); + + + it('should allow reverting to common settings for visibleCol', async function() { + // Same as above for reverting 'visiblecCol'. + await gu.clickCell({section: 'C', rowNum: 2, col: 0}); + await gu.setVisibleCol('Last Name'); + await checkSections({rowNum: 2, col: 0}, () => $('.test-fbuilder-ref-col-select .test-select-row').text(), { + A: ['Klein, Cordelia', 'Full Name'], + B: ['Klein, Cordelia', 'Full Name'], + C: ['Klein', 'Last Name'], + }); + await gu.userActionsCollect(true); + await gu.fieldSettingsRevertToCommon(); + await checkSections({rowNum: 2, col: 0}, () => $('.test-fbuilder-ref-col-select .test-select-row').text(), { + A: ['Klein, Cordelia', 'Full Name'], + B: ['Klein, Cordelia', 'Full Name'], + C: ['Klein, Cordelia', 'Full Name'], + }); + await gu.userActionsVerify([ + ["UpdateRecord", "_grist_Views_section_field", 141, {"widgetOptions":""}], + ["UpdateRecord", "_grist_Views_section_field", 141, {"visibleCol":0}], + ["SetDisplayFormula", "Rates", 141, null, ""], + ]); + await gu.undo(2); + }); + + + it('should allow saving separate settings as common', async function() { + // Change column C to use different settings from A. + await gu.clickCell({section: 'C', rowNum: 1, col: 1}); + await gu.dateFormat('MMMM Do, YYYY'); + await checkSections({rowNum: 1, col: 1}, () => gu.dateFormat(), { + A: ['2012-01-02', 'YYYY-MM-DD'], + B: ['2012-01-02', 'YYYY-MM-DD'], + C: ['January 2nd, 2012', 'MMMM Do, YYYY'], + }); + + // Save C settings as common settings. Check that A and B now match. + await gu.userActionsCollect(true); + await gu.fieldSettingsSaveAsCommon(); + await checkSections({rowNum: 1, col: 1}, () => gu.dateFormat(), { + A: ['January 2nd, 2012', 'MMMM Do, YYYY'], + B: ['January 2nd, 2012', 'MMMM Do, YYYY'], + C: ['January 2nd, 2012', 'MMMM Do, YYYY'], + }); + await gu.userActionsVerify([ + ["UpdateRecord", "_grist_Tables_column", 15, {"widgetOptions": + '{"widget":"TextBox","dateFormat":"MMMM Do, YYYY","isCustomDateFormat":false,"alignment":"left"}'}], + ["UpdateRecord", "_grist_Views_section_field", 145, {"widgetOptions":""}], + ]); + await gu.undo(2); + await checkSections({rowNum: 1, col: 1}, () => gu.dateFormat(), { + A: ['2012-01-02', 'YYYY-MM-DD'], + B: ['2012-01-02', 'YYYY-MM-DD'], + C: ['2012-01-02', 'YYYY-MM-DD'], + }); + }); + + + it('should allow saving separate settings as common for visibleCol', async function() { + // Same as above for saving 'visiblecCol'. + await gu.clickCell({section: 'C', rowNum: 2, col: 0}); + await gu.setVisibleCol('Last Name'); + await checkSections({rowNum: 2, col: 0}, () => $('.test-fbuilder-ref-col-select .test-select-row').text(), { + A: ['Klein, Cordelia', 'Full Name'], + B: ['Klein, Cordelia', 'Full Name'], + C: ['Klein', 'Last Name'], + }); + await gu.userActionsCollect(true); + await gu.fieldSettingsSaveAsCommon(); + await checkSections({rowNum: 2, col: 0}, () => $('.test-fbuilder-ref-col-select .test-select-row').text(), { + A: ['Klein', 'Last Name'], + B: ['Klein', 'Last Name'], + C: ['Klein', 'Last Name'], + }); + await gu.userActionsVerify([ + ["UpdateRecord", "_grist_Tables_column", 12, {"visibleCol":3}], + ["SetDisplayFormula", "Rates", null, 12, "$Person.Last_Name"], + ["UpdateRecord", "_grist_Views_section_field", 141, {"widgetOptions":""}], + ["UpdateRecord", "_grist_Views_section_field", 141, {"visibleCol":0}], + ["SetDisplayFormula", "Rates", 141, null, ""], + ]); + await gu.undo(2); + await checkSections({rowNum: 2, col: 0}, () => $('.test-fbuilder-ref-col-select .test-select-row').text(), { + A: ['Klein, Cordelia', 'Full Name'], + B: ['Klein, Cordelia', 'Full Name'], + C: ['Klein, Cordelia', 'Full Name'], + }); + }); +}); diff --git a/test/nbrowser/FillLinkedRecords.ntest.js b/test/nbrowser/FillLinkedRecords.ntest.js new file mode 100644 index 00000000..bb124994 --- /dev/null +++ b/test/nbrowser/FillLinkedRecords.ntest.js @@ -0,0 +1,148 @@ +import { assert } from 'mocha-webdriver'; +import { $, gu, test } from 'test/nbrowser/gristUtil-nbrowser'; + +/** + * This test verifies that when a section is auto-filtered using section-linking, newly added + * records automatically get assigned the filter value. + */ +describe('FillLinkedRecords.ntest', function() { + const cleanup = test.setupTestSuite(this); + + gu.bigScreen(); + + before(async function() { + await gu.supportOldTimeyTestCode(); + await gu.useFixtureDoc(cleanup, "Favorite_Films.grist", true); + await gu.toggleSidePanel("left", "close"); + }); + + afterEach(function() { + return gu.checkForErrors(); + }); + + it('should auto-fill values when typing into add-row', async function() { + await gu.openSidePane('view'); + await $('.test-config-data').click(); + await gu.actions.selectTabView('All'); + + // Link the sections first since the sample document start with no links. + // Connect Friends -> Films + await gu.getSection('Films record').click(); + await $('.test-right-select-by').click(); + await $('.test-select-row:contains(Friends record)').click(); + await gu.waitForServer(); + + // Connect Films -> Performances grid + await gu.getSection('Performances record').click(); + await $('.test-right-select-by').click(); + await $('.test-select-row:contains(Films record)').click(); + await gu.waitForServer(); + + // Connect Films -> Performances detail + await gu.getSection('Performances detail').click(); + await $('.test-right-select-by').click(); + await $('.test-select-row:contains(Films record)').click(); + await gu.waitForServer(); + + // Now pick a movie, and select the Performances grid. + await gu.clickCell({section: 'Films record', col: 0, rowNum: 2}); + await gu.actions.viewSection('Performances record').selectSection(); + + // It should have just two records initially, with an Add-New row. + assert.equal(await gu.getGridLastRowText(), '3'); + assert.deepEqual(await gu.getGridValues({cols: [0, 1], rowNums: [2, 3]}), [ + 'Robin Wright', 'Forrest Gump', + '', '']); + + // Add a record, and ensure it shows up, and has Film auto-filled in. + await gu.userActionsCollect(true); + await gu.addRecord(['Rebecca Williams']); + await gu.userActionsVerify([ + ["AddRecord", "Performances", null, {"Actor": "Rebecca Williams", "Film": 2}] + ]); + assert.deepEqual(await gu.getGridValues({cols: [0, 1], rowNums: [2, 3]}), [ + 'Robin Wright', 'Forrest Gump', + 'Rebecca Williams', 'Forrest Gump']); + assert.equal(await gu.getGridLastRowText(), '4'); + }); + + it('should auto-fill values when inserting records', async function() { + // Click another movie, and check the values we see. + await gu.clickCell({section: 'Films record', col: 0, rowNum: 5}); + await gu.actions.viewSection('Performances record').selectSection(); + assert.deepEqual(await gu.getGridValues({cols: [0, 1], rowNums: [1, 2]}), [ + 'Christian Bale', 'The Dark Knight', + 'Heath Ledger', 'The Dark Knight' + ]); + assert.equal(await gu.getGridLastRowText(), '3'); + + // Add a couple of records in Performances grid using keyboard shortcuts. + await gu.clickCell({col: 0, rowNum: 3}); + await gu.sendKeys([$.MOD, $.SHIFT, $.ENTER]); + await gu.clickCell({col: 0, rowNum: 1}); + await gu.sendKeys([$.MOD, $.ENTER]); + await gu.waitForServer(); + + // Verify they are shown where expected with Film filled in. + assert.deepEqual(await gu.getGridValues({cols: [0, 1], rowNums: [1, 2, 3, 4]}), [ + 'Christian Bale', 'The Dark Knight', + '', 'The Dark Knight', + 'Heath Ledger', 'The Dark Knight', + '', 'The Dark Knight', + ]); + assert.equal(await gu.getGridLastRowText(), '5'); + + // Add a record in Performances detail using keyboard shortcuts. + await gu.actions.viewSection('Performances detail').selectSection(); + assert.deepEqual(await gu.getDetailValues({cols: ['Actor', 'Film'], rowNums: [1]}), + ['Christian Bale', 'The Dark Knight']); + await gu.sendKeys([$.MOD, $.ENTER]); + await gu.waitForServer(); + + // Verify the record is shown with Film filled in, and added to the grid section too. + // Note: rowNum needs to be 1 now for card views without row numbers shown. + assert.deepEqual(await gu.getDetailValues({cols: ['Actor', 'Film'], rowNums: [1]}), + ['', 'The Dark Knight']); + + await gu.actions.viewSection('Performances record').selectSection(); + assert.deepEqual(await gu.getGridValues({cols: [0, 1], rowNums: [1, 2, 3, 4, 5]}), [ + 'Christian Bale', 'The Dark Knight', + '', 'The Dark Knight', + '', 'The Dark Knight', + 'Heath Ledger', 'The Dark Knight', + '', 'The Dark Knight', + ]); + assert.equal(await gu.getGridLastRowText(), '6'); + + // Undo the record insertions. + await gu.undo(3); + }); + + it('should auto-fill when pasting data', async function() { + // Click a movie, and check the values we expect to start with. + await gu.clickCell({section: 'Films record', col: 0, rowNum: 6}); + await gu.actions.viewSection('Performances record').selectSection(); + assert.deepEqual(await gu.getGridValues({cols: [0, 1, 2], rowNums: [1, 4]}), [ + 'Chris Evans', 'The Avengers', 'Steve Rogers', + 'Scarlett Johansson', 'The Avengers', 'Natasha Romanoff', + ]); + assert.equal(await gu.getGridLastRowText(), '5'); + + // 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.waitForServer(); + + // Verify that three new rows now show up, with Film auto-filled. + assert.deepEqual(await gu.getGridValues({cols: [0, 1, 2], rowNums: [1, 4, 5, 6, 7]}), [ + 'Chris Evans', 'The Avengers', 'Steve Rogers', + 'Scarlett Johansson', 'The Avengers', 'Natasha Romanoff', + '', 'The Avengers', 'Steve Rogers', + '', 'The Avengers', 'Tony Stark', + '', 'The Avengers', 'Bruce Banner', + ]); + assert.equal(await gu.getGridLastRowText(), '8'); + }); +}); diff --git a/test/nbrowser/GridOptions.ntest.js b/test/nbrowser/GridOptions.ntest.js new file mode 100644 index 00000000..de0780a0 --- /dev/null +++ b/test/nbrowser/GridOptions.ntest.js @@ -0,0 +1,126 @@ +/** + * NOTE: This test is migrated to new UI as test/nbrowser/GridOptions.ts. + * Remove this version once old UI is no longer supported. + */ + + +import { assert, driver } from 'mocha-webdriver'; +import { $, gu, test } from 'test/nbrowser/gristUtil-nbrowser'; + +describe("GridOptions.ntest", function() { + const cleanup = test.setupTestSuite(this); + + + // ====== Some Helpers ====== + + let secNames = ["COUNTRY", "CITY", "COUNTRYLANGUAGE"]; + let switchTo = (i) => + gu.actions.viewSection(secNames[i]).selectSection(); + + /* Test that styles on the given section match the specified flags + * sec: index into secNames + * hor/vert/zebra: boolean flags + */ + async function assertHVZ(sec, hor, vert, zebra) { + let testClasses = + ['record-hlines', 'record-vlines', 'record-zebra']; + let flags = [hor, vert, zebra]; + + let cell = await gu.getCell({rowNum: 1, col: 0, section: secNames[sec]}); + let row = await cell.findClosest('.record'); + const rowClasses = await row.classList(); + testClasses.forEach( (cls, i) => { + if(flags[i]) { assert.include(rowClasses, cls);} + else { assert.notInclude(rowClasses, cls); } + }); + } + + + // ====== Prepare Document ====== + + before(async function() { + await gu.supportOldTimeyTestCode(); + await gu.useFixtureDoc(cleanup, "World-v10.grist", true); + await $('.test-gristdoc').wait(); + }); + + beforeEach(async function() { + //Prepare consistent view + await gu.actions.selectTabView("Country"); + await gu.openSidePane('view'); + await $(".test-grid-options").wait(assert.isDisplayed); + }); + + afterEach(function() { + return gu.checkForErrors(); + }); + + + // ====== MAIN TESTS ====== + + + it('should only be visible on grid view/summary view', async function() { + + let getOptions = () => $(".test-grid-options"); + await assert.isPresent(getOptions()); + + // check that it doesnt show up in detail view + await gu.actions.viewSection("COUNTRY Card List").selectSection(); + await assert.isPresent(getOptions(), false); + + // check that it shows up on the grid-views + await gu.actions.viewSection("COUNTRY").selectSection(); + await assert.isDisplayed(getOptions()); + await gu.actions.viewSection("CITY").selectSection(); + await assert.isDisplayed(getOptions()); + await gu.actions.viewSection("COUNTRYLANGUAGE").selectSection(); + await assert.isDisplayed(getOptions()); + + }); + + it('should set and persist styles on a grid', async function() { + + // get handles on elements + let h = ".test-h-grid-button input"; + let v = ".test-v-grid-button input"; + let z = ".test-zebra-stripe-button input"; + + // should start with v+h gridlines, no zebra + await assertHVZ(0, true, true, false); + + // change values on all the sections + await switchTo(0); + await $(z).scrollIntoView().click(); + + await switchTo(1); + await $(h).click(); + await $(v).click(); + + await switchTo(2); + await $(h).click(); // turn off + await $(z).click(); // turn on + await gu.waitForServer(); + + await assertHVZ(0, true, true, true); // all on + await assertHVZ(1, false, false, false); // all off + await assertHVZ(2, false, true, true); // -h +v +z + + // ensure that values persist after reload + await driver.navigate().refresh(); + //await $.injectIntoPage(); + await gu.waitForDocToLoad(); + await assertHVZ(0, true, true, true); // all on + await assertHVZ(1, false, false, false); // all off + await assertHVZ(2, false, true, true); // -h +v +z + }); + + + it('should set .record-even on even-numbered rows', async function() { + let rowClasses = row => + gu.getCell({rowNum: row, col: 0}).closest('.record').classList(); + + await switchTo(0); + assert.notInclude(await rowClasses(1), 'record-even', "row 1 should be odd"); + assert.include(await rowClasses(2), 'record-even', "row 2 should be even"); + }); +}); diff --git a/test/nbrowser/Health.ntest.js b/test/nbrowser/Health.ntest.js new file mode 100644 index 00000000..ef03c23e --- /dev/null +++ b/test/nbrowser/Health.ntest.js @@ -0,0 +1,17 @@ +import { assert, driver } from 'mocha-webdriver'; +import { gu, server, test } from 'test/nbrowser/gristUtil-nbrowser'; + +describe('Health.ntest', function() { + test.setupTestSuite(this); + + before(async function() { + await gu.supportOldTimeyTestCode(); + }); + + it('make sure the health check endpoint returns something', async function() { + await driver.get(server.getHost() + "/status") + const txt = await driver.getPageSource(); + assert.match(txt, /Grist .* is alive/); + }); + +}); diff --git a/test/nbrowser/Localization.ts b/test/nbrowser/Localization.ts index d7918d50..43da5800 100644 --- a/test/nbrowser/Localization.ts +++ b/test/nbrowser/Localization.ts @@ -9,7 +9,7 @@ import os from "os"; import path from 'path'; describe("Localization", function() { - this.timeout(20000); + this.timeout(60000); setupTestSuite(); before(async function() { diff --git a/test/nbrowser/NewDocument.ntest.js b/test/nbrowser/NewDocument.ntest.js new file mode 100644 index 00000000..c81c3f72 --- /dev/null +++ b/test/nbrowser/NewDocument.ntest.js @@ -0,0 +1,161 @@ +/* global window */ + +import { assert, driver } from 'mocha-webdriver'; +import { $, gu, test } from 'test/nbrowser/gristUtil-nbrowser'; + +describe('NewDocument.ntest', function() { + test.setupTestSuite(this); + + before(async function() { + await gu.supportOldTimeyTestCode(); + }); + + afterEach(function() { + return gu.checkForErrors(); + }); + + it('should create new Untitled document', async function() { + this.timeout(10000); + await gu.actions.createNewDoc('Untitled'); + assert.equal(await gu.actions.getDocTitle(), 'Untitled'); + assert.equal(await driver.getTitle(), 'Untitled - Grist'); + assert.equal(await $('.active_section .test-viewsection-title').wait().text(), 'TABLE1'); + await gu.waitForServer(); + }); + + it('should start with a 1x3 grid', async function() { + await $('.record.record-add').wait(); + assert.lengthOf(await $('.grid_view_data .record:not(.column_names)').array(), 1, 'should have 1 row ("add" row)'); + assert.lengthOf(await $('.column_names .column_name').array(), 4, 'should have 3 columns and 1 "add" column'); + }); + + it('should have first cell selected', async function() { + assert.isDisplayed(await gu.getCellRC(0, 0).find('.active_cursor')); + }); + + it('should open notify toasts on errors', async function() { + // Verify that uncaught exceptions and errors from server cause the notifications box to open. + + // For a plain browser error, we attach an error-throwing handler to click-on-logo. + await driver.executeScript( + 'setTimeout(() => window.gristApp.testTriggerError("Our fake error"))', 0); + + // Wait for the notifications window to open and check it has the error we expect. + await $('.test-notifier-toast-message').wait(1, assert.isDisplayed); + assert.match(await $('.test-notifier-toast-message').last().text(), /Our fake error/); + + // Close the notifications window. + await $(".test-notifier-toast-close").click(); + await assert.isPresent($('.test-notifier-toast-message'), false); + + // Try a server command that should fail. We need a reasonble timeout for executeAsyncScript. + await driver.manage().setTimeouts({script: 500}); + let result = await driver.executeAsyncScript(() => { + var cb = arguments[arguments.length - 1]; + window.gristApp.comm.getDocList() + .then( + newName => cb("unexpected success"), + err => { cb(err.toString()); throw err; } + ); + }); + assert.match(result, /Unknown method getDocList/); + + // Now make sure the notifications window is open and has the error we expect. + await assert.isDisplayed($('.test-notifier-toast-message')); + assert.match(await $('.test-notifier-toast-message').last().text(), /Unknown method getDocList/); + + // Close the notifications window. + await $(".test-notifier-toast-close").click(); + await assert.isPresent($('.test-notifier-toast-message'), false); + + assert.deepEqual(await driver.executeScript(() => window.getAppErrors()), + ['Our fake error', 'Unknown method getDocList']); + await driver.executeScript( + 'setTimeout(() => window.gristApp.topAppModel.notifier.clearAppErrors())'); + }); + + describe('Cell editing', function() { + + it('should add rows on entering new data', async function() { + assert.equal(await gu.getGridRowCount(), 1); + await gu.getCellRC(0, 0).click(); + await gu.sendKeys('hello', $.ENTER); + await gu.waitForServer(); + await gu.getCellRC(1, 1).click(); + await gu.sendKeys('world', $.ENTER); + await gu.waitForServer(); + assert.equal(await gu.getGridRowCount(), 3); + }); + + it('should edit on Enter, cancel on Escape, save on Enter', async function() { + var cell_1_b = gu.getCellRC(0, 1); + assert.equal(await cell_1_b.text(), ''); + await cell_1_b.click(); + + await gu.sendKeys($.ENTER); + await $('.test-widget-text-editor').wait(); + await gu.sendKeys('foo', $.ESCAPE); + await gu.waitForServer(); + assert.equal(await cell_1_b.text(), ''); + + await gu.sendKeys($.ENTER); + await $('.test-widget-text-editor').wait(); + await gu.sendKeys('bar', $.ENTER); + await gu.waitForServer(); + assert.equal(await cell_1_b.text(), 'bar'); + }); + + it('should append to cell with content on Enter', async function() { + var cell_1_a = gu.getCellRC(0, 0); + assert.equal(await cell_1_a.text(), 'hello'); + await cell_1_a.click(); + + await gu.sendKeys($.ENTER); + await $('.test-widget-text-editor').wait(); + assert.equal(await $('.test-widget-text-editor textarea').val(), 'hello'); + await gu.sendKeys(', world!', $.ENTER); + await gu.waitForServer(); + + assert.equal(await cell_1_a.text(), 'hello, world!'); + }); + + it('should clear data in selected cells on Backspace and Delete', async function() { + let testDelete = async function(delKey) { + // should clear a single cell + var cell_1_a = gu.getCellRC(0, 0); + await cell_1_a.click(); + await gu.sendKeys('A1', $.ENTER); + await gu.waitForServer(); + assert.equal(await cell_1_a.text(), 'A1'); + await cell_1_a.click(); + await gu.sendKeys(delKey); + await gu.waitForServer(); + assert.equal(await cell_1_a.text(), ''); + + // should clear a selection of cells + await gu.enterGridValues(0, 0, [['A1', 'A2'], ['B1', 'B2']]); + await gu.waitForServer(); + assert.deepEqual(await gu.getGridValues({ rowNums: [1, 2], cols: [0, 1] }), ['A1', 'B1', 'A2', 'B2']); + await cell_1_a.click(); + await gu.sendKeys([$.SHIFT, $.RIGHT], [$.SHIFT, $.DOWN], delKey); + await gu.waitForServer(); + assert.deepEqual(await gu.getGridValues({ rowNums: [1, 2], cols: [0, 1] }), ['', '', '', '']); + + // should clear a selection of cells with a formula column + await gu.enterGridValues(0, 0, [['A1', 'A2'], ['B1', 'B2']]); + await gu.clickCellRC(0, 2); + await gu.sendKeys('=', '$A', $.ENTER); + await gu.waitForServer(); + assert.deepEqual(await gu.getGridValues({ rowNums: [1, 2], cols: [0, 1, 2] }), + ['A1', 'B1', 'A1', 'A2', 'B2', 'A2']); + await gu.clickCellRC(0, 1); + await gu.sendKeys([$.SHIFT, $.RIGHT], [$.SHIFT, $.DOWN], delKey); + await gu.waitForServer(); + assert.deepEqual(await gu.getGridValues({ rowNums: [1, 2], cols: [0, 1, 2] }), + [ 'A1', '', 'A1', 'A2', '', 'A2' ]); + }; + await testDelete($.BACK_SPACE); + await testDelete($.DELETE); + }); + }); +}); diff --git a/test/nbrowser/Pages.ts b/test/nbrowser/Pages.ts index 1b786e9b..81d98310 100644 --- a/test/nbrowser/Pages.ts +++ b/test/nbrowser/Pages.ts @@ -7,7 +7,7 @@ import {server, setupTestSuite} from 'test/nbrowser/testUtils'; import values = require('lodash/values'); describe('Pages', function() { - this.timeout(30000); + this.timeout(60000); setupTestSuite(); let doc: DocCreationInfo; let api: UserAPI; diff --git a/test/nbrowser/Properties.ntest.js b/test/nbrowser/Properties.ntest.js new file mode 100644 index 00000000..d15b465a --- /dev/null +++ b/test/nbrowser/Properties.ntest.js @@ -0,0 +1,151 @@ +import { assert } from 'mocha-webdriver'; +import { $, gu, test } from 'test/nbrowser/gristUtil-nbrowser'; + +describe('Properties.ntest', function() { + const cleanup = test.setupTestSuite(this); + before(async function() { + await gu.supportOldTimeyTestCode(); + await gu.useFixtureDoc(cleanup, "Hello.grist", true); + }); + + afterEach(function() { + return gu.checkForErrors(); + }); + + it("webdriver should handle parens and other keys", async function() { + // This isn't a test of properties really, but a test of Selenium: speficically that the + // workaround for Selenium bugs in gu.sendKeys actually works. + await $("$GridView_columnLabel").first().click(); + + // We'll undo afterwards, and verify that we got the same text back. + var text = await gu.getCellRC(0, 0).text(); + + var specialChars = "()[]{}~!@#$%^&*-_=+/?><.,'\";:"; + await gu.sendKeys(specialChars + specialChars, $.ENTER); + await gu.waitForServer(); + assert.equal(await gu.getCellRC(0, 0).text(), specialChars + specialChars); + + // Undo and compare to previous value. + await gu.sendKeys([$.MOD, 'z']); + await gu.waitForServer(); + assert.equal(await gu.getCellRC(0, 0).text(), text); + }); + + it("cells should indicate when conversion fails for a value", async function() { + await $("$GridView_columnLabel:nth-child(2)").click(); + + // Fill in a column of values, some numeric, some not. + await gu.enterGridValues(0, 1, [["17", "foo", "", "-100"]]); + await gu.waitForServer(); + + assert.deepEqual(await gu.getVisibleGridCells(1, [1, 2, 3, 4]), + ["17", "foo", "", "-100"]); + await $("$GridView_columnLabel:nth-child(2)").click(); + + // Convert the column to Numeric. + await gu.openSidePane('field'); + assert.equal(await $(".test-field-label").val(), 'B'); + await gu.setType('Numeric'); + await $('.test-type-transform-apply').wait().click(); + + assert.deepEqual(await gu.getVisibleGridCells(1, [1, 2, 3, 4]), + ["17", "foo", "", "-100"]); + + // Undo of conversion should restore old values. + await gu.undo(); + await $(".test-fbuilder-type-select .test-select-row:contains(Text)").wait(); + assert.deepEqual(await gu.getVisibleGridCells(1, [1, 2, 3, 4]), + ["17", "foo", "", "-100"]); + + // Redo should work too. + await $(".test-redo").click(); + await $(".test-fbuilder-type-select .test-select-row:contains(Numeric)").wait(); + await gu.waitForServer(); + assert.deepEqual(await gu.getVisibleGridCells(1, [1, 2, 3, 4]), + ["17", "foo", "", "-100"]); + }); + + it("cells should indicate when new value is wrong type", async function() { + // Go to column "c", and change type to Numeric. + await $("$GridView_columnLabel:nth-child(3)").click(); + assert.equal(await $(".test-field-label").val(), 'C'); + await gu.setType('Numeric'); + await $('.test-type-transform-apply').wait().click(); + await gu.waitForServer(); + + // Remove focus from FieldBuilder type dropdown, so that sentKeys go to the main app. + await $("body").click(); + + // Fill in a column of values, some numeric, some not. + await gu.enterGridValues(0, 2, [["25", "", "bar", "-123"]]); + await gu.waitForServer(); + + // TODO: The 0.00 might be wrong behavior: we probably want an empty cell here, although when + // converting an empty text cell to numeric, we want it to become 0. In other words, not all + // conversions are the same. + assert.deepEqual(await gu.getVisibleGridCells(2, [1, 2, 3, 4]), + ["25", "", "bar", "-123"]); + assert.deepEqual(await gu.getVisibleGridCells({ + col: 2, + rowNums: [1, 2, 3, 4], + mapper: e => e.find('.field_clip').hasClass('invalid'), + }), [false, false, true, false]); + + // Select the column again, and type in values in a different order. Ensure the cells change + // appropriately. + await $("$GridView_columnLabel:nth-child(3)").click(); + await gu.enterGridValues(0, 2, [["", "bar", "-123", "25"]]); + await gu.waitForServer(); + + // TODO: The first cell might be wrong behavior; we probably want an empty cell after DELETE. + assert.deepEqual(await gu.getVisibleGridCells(2, [1, 2, 3, 4]), + ["", "bar", "-123", "25"]); + assert.deepEqual(await gu.getVisibleGridCells({ + col: 2, + rowNums: [1, 2, 3, 4], + mapper: e => e.find('.field_clip').hasClass('invalid') + }), [false, true, false, false]); + }); + + it("formula errors should be indicated", async function() { + // Go to column "E", and change formula to eval column "D". + await $("$GridView_columnLabel:nth-child(5)").click(); + await gu.sendKeys("eval($D)", $.ENTER); + // Fill in a bunch of formula text for the "eval" formula to try. This is a way to get a whole + // bunch of different errors in one columns. + await $("$GridView_columnLabel:nth-child(4)").click(); + assert.equal(await $(".test-field-label").val(), 'D'); + await gu.setType('Text'); + + await gu.enterGridValues(0, 3, [[ + "25", + "", + "asdf", + "ValueError()", + "__import__('sys').exit(3)", + 'u"résumé 三"', + "12/(2-1-1)", + "[1,2,3]"]]); + await gu.waitForServer(); + + assert.deepEqual(await gu.getVisibleGridCells(4, [1, 2, 3, 4, 5, 6, 7, 8]), [ + "25", + "#SyntaxError", + "#NameError", + "ValueError()", + "#SystemExit", + 'résumé 三', + "#DIV/0!", + "1, 2, 3", + ]); + + assert.deepEqual( + await gu.getVisibleGridCells({ + col: 4, + rowNums: [1, 2, 3, 4, 5, 6, 7, 8], + mapper: e => e.find('.field_clip').hasClass("invalid") + }), + // Last one (list) is valid because lists are a supported type of value. + [false, true, true, true, true, false, true, false]); + }); +}); diff --git a/test/nbrowser/ReferenceList.ts b/test/nbrowser/ReferenceList.ts index ce5ce710..7c14f7c8 100644 --- a/test/nbrowser/ReferenceList.ts +++ b/test/nbrowser/ReferenceList.ts @@ -4,7 +4,7 @@ import {server, setupTestSuite} from 'test/nbrowser/testUtils'; import {Session} from 'test/nbrowser/gristUtils'; describe('ReferenceList', function() { - this.timeout(20000); + this.timeout(60000); setupTestSuite(); let session: Session; const cleanup = setupTestSuite({team: true}); diff --git a/test/nbrowser/SavePosition.ntest.js b/test/nbrowser/SavePosition.ntest.js new file mode 100644 index 00000000..bce9d9bb --- /dev/null +++ b/test/nbrowser/SavePosition.ntest.js @@ -0,0 +1,106 @@ +import { assert } from 'mocha-webdriver'; +import { $, gu, test } from 'test/nbrowser/gristUtil-nbrowser'; + +describe('SavePosition.ntest', function() { + const cleanup = test.setupTestSuite(this); + + before(async function() { + this.timeout(Math.max(this.timeout(), 20000)); // Long-running test, unfortunately + await gu.supportOldTimeyTestCode(); + await gu.useFixtureDoc(cleanup, "World.grist", true); + }); + + afterEach(function() { + return gu.checkForErrors(); + }); + + it('should maintain cursor and scroll positions when switching between views', async function() { + var recordSection = await gu.actions.viewSection('City'); + var cardSection = await gu.actions.viewSection('City Card List'); + var cardScrollPane = $('.detailview_scroll_pane'); + + // Set up scroll linking between the two sections. + await gu.openSidePane('view'); + await $('.test-config-data').click(); + + // Connect CITY -> CITY Card List + await gu.getSection('CITY Card List').click(); + await $('.test-right-select-by').click(); + await $('.test-select-row:contains(CITY)').click(); + await gu.waitForServer(); + await gu.closeSidePane(); + + await recordSection.selectSection(); + + // Click on the District cell with row number 8. + await gu.clickCellRC(7, 2); + // Scroll to the Population cell with row number 22. + await gu.getCellRC(21, 3).scrollIntoView(); + + // Switch to card section, make a cursor selection and scroll selection. + await cardSection.selectSection(); + + var desiredCard = await cardScrollPane.findOldTimey('.g_record_detail .detail_row_num:contains(3150)').parent().elem(); + var desiredField = await desiredCard.findOldTimey('.g_record_detail_label:contains(Country)').parent().parent(); + await desiredField.click(); + await cardScrollPane.findOldTimey('.g_record_detail .detail_row_num:contains(3142)').scrollIntoView(); + + // Switch tabs back and forth. + await gu.actions.selectTabView('Country'); + await gu.actions.selectTabView('City'); + + // Assert that the cursor position in the card section is the same. + desiredCard = await cardScrollPane.findOldTimey('.g_record_detail .detail_row_num:contains(3150)').parent().elem(); + desiredField = await desiredCard.findOldTimey('.g_record_detail_label:contains(Country)').parent().parent(); + await assert.hasClass(desiredField.find('.selected_cursor'), 'active_cursor'); + + // Assert that the element that was scrolled into view is still displayed. + await assert.isDisplayed(cardScrollPane.findOldTimey('.g_record_detail .detail_row_num:contains(3142)')); + + await recordSection.selectSection(); + + // Assert that the scroll position in the grid section is unchanged. + await assert.isDisplayed(gu.getCellRC(21, 3)); + + // Assert that the cursor position in the grid section is the same. + await gu.scrollActiveViewTop(); + await gu.getCellRC(0, 0).wait(assert.isDisplayed); + assert.deepEqual(await gu.getCursorPosition(), { rowNum: 8, col: 2 }); + }); + + it('should maintain cursor with linked sections', async function() { + // Switch to view 'Country' (which has several linked sections). + await gu.actions.selectTabView('Country'); + + // Select a position to the cursor in each section. + await gu.getCell({col: 1, rowNum: 9, section: 'Country'}).click(); + await gu.getCell({col: 0, rowNum: 6, section: 'City'}).click(); + await gu.getCell({col: 2, rowNum: 2, section: 'CountryLanguage'}).click(); + await gu.getDetailCell({col: 'IndepYear', rowNum: 1, section: 'Country Card List'}).click(); + + // Switch tabs back and forth. + await gu.actions.selectTabView('City'); + await gu.actions.selectTabView('Country'); + + // Verify the cursor positions. + assert.deepEqual(await gu.getCursorPosition('Country'), + {rowNum: 9, col: 1}); + assert.deepEqual(await gu.getCursorPosition('City'), + {rowNum: 6, col: 0}); + assert.deepEqual(await gu.getCursorPosition('CountryLanguage'), + {rowNum: 2, col: 2}); + assert.deepEqual(await gu.getCursorPosition('Country Card List'), + {rowNum: 1, col: 'IndepYear'}); + }); + + 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 gu.waitForServer(); + assert.deepEqual(await gu.getVisibleGridCells(1, [8, 9, 10]), + ['United Arab Emirates', 'Pará', 'Armenia']); + }); +}); diff --git a/test/nbrowser/SortDates.ntest.js b/test/nbrowser/SortDates.ntest.js new file mode 100644 index 00000000..bc233a93 --- /dev/null +++ b/test/nbrowser/SortDates.ntest.js @@ -0,0 +1,141 @@ +import { assert } from 'mocha-webdriver'; +import { $, gu, test } from 'test/nbrowser/gristUtil-nbrowser'; + +// Helper that returns the cell text prefixed by "!" if the cell is invalid. +async function valText(cell) { + const isInvalid = await cell.find('.field_clip').hasClass("invalid"); + const text = await cell.getText(); + return (isInvalid ? "!" : "") + text; +} + +async function clickColumnMenuSort(colName, itemText) { + // Scroll into view doesn't work on Grid because the first column + // will always be hidden behind row number element. So we will always + // move to the first column before opening menu, as scrolling right + // does work (there are no absolute positioned elements there). + await gu.sendKeys($.HOME); + await gu.openColumnMenu(colName); + const dir = (itemText === 'Sort ascending') ? 'asc' : 'dsc'; + return $(`.grist-floating-menu .test-sort-${dir}`).click(); +} + +describe('SortDates.ntest', function() { + const cleanup = test.setupTestSuite(this); + before(async function() { + await gu.supportOldTimeyTestCode(); + await gu.useFixtureDoc(cleanup, "SortDates.grist", true); + }); + + afterEach(function() { + return gu.checkForErrors(); + }); + + it("should display calculated DateTimes as valid", async function() { + // Check that Dates and DateTimes returned from 'Any' formulas are displayed + // as valid (rather than pink error values). + assert.deepEqual(await gu.getGridValues({rowNums: [1, 2, 3, 4, 5, 6, 7], cols: [0, 1, 2, 3], mapper: valText}), [ + '2017-04-11', '2017-04-11 9:30am', '2017-04-12', '2017-04-12 09:30:00-04:00', + '2017-07-13', '2017-07-13 4:00am', '2017-07-14', '2017-07-14 04:00:00-04:00', + '!invalid1', '!invalid2', '!#TypeError', '!#TypeError', + '2017-05-01', '2017-05-01 7:00am', '2017-05-02', '2017-05-02 07:00:00-04:00', + '2017-04-21', '2017-04-21 12:00pm', '2017-04-22', '2017-04-22 12:00:00-04:00', + '', '', '', '', + '2017-03-16', '2017-03-16 4:00pm', '2017-03-17', '2017-03-17 16:00:00-04:00', + ]); + }); + + it('should sort correctly by Date or DateTime', async function() { + // Check that we sort by the Date and DateTime column works as expected, even + // when blanks or AltText is present. + await gu.openSidePane('view'); + await gu.toggleSidePanel('left', 'close'); + await $('.test-config-sortAndFilter').click(); + + // Sort by a special column first to rearrange. It's specially chosen to trigger some + // previously incorrect comparisons that may cause wrong order. (The actual bug only existed + // at the time of writing in the test case for Any formula columns returning Dates/DateTimes.) + await clickColumnMenuSort('Order', 'Sort ascending'); + let orderRow = await $(".test-sort-config-row:contains(Order)").wait().elem(); + await assert.isPresent(orderRow); + await assert.isPresent(orderRow.find(".test-sort-config-sort-order-asc")); + await gu.getColumnHeader('Date').scrollIntoView({inline: "end"}); + await clickColumnMenuSort('Date', 'Sort ascending'); + const dateRow = await $(".test-sort-config-row:contains(Date)").wait().elem(); + await assert.isPresent(dateRow); + await assert.isPresent(dateRow.find(".test-sort-config-sort-order-asc")); + + // Check that the data is now sorted. + assert.deepEqual(await gu.getGridValues({rowNums: [1, 2, 3, 4, 5, 6, 7], cols: [0, 1, 2, 3], mapper: valText}), [ + '2017-03-16', '2017-03-16 4:00pm', '2017-03-17', '2017-03-17 16:00:00-04:00', + '2017-04-11', '2017-04-11 9:30am', '2017-04-12', '2017-04-12 09:30:00-04:00', + '2017-04-21', '2017-04-21 12:00pm', '2017-04-22', '2017-04-22 12:00:00-04:00', + '2017-05-01', '2017-05-01 7:00am', '2017-05-02', '2017-05-02 07:00:00-04:00', + '2017-07-13', '2017-07-13 4:00am', '2017-07-14', '2017-07-14 04:00:00-04:00', + '', '', '', '', + '!invalid1', '!invalid2', '!#TypeError', '!#TypeError', + ]); + + await clickColumnMenuSort('Order', 'Sort ascending'); + orderRow = await $(".test-sort-config-row:contains(Order)").wait().elem(); + await assert.isPresent(orderRow); + await assert.isPresent(orderRow.find(".test-sort-config-sort-order-asc")); + await clickColumnMenuSort('DTime', 'Sort descending'); + const dtimeRow = await $(".test-sort-config-row:contains(DTime)").wait().elem(); + await assert.isPresent(dtimeRow); + await assert.isPresent(dtimeRow.find(".test-sort-config-sort-order-desc")); + + assert.deepEqual(await gu.getGridValues({rowNums: [1, 2, 3, 4, 5, 6, 7], cols: [0, 1, 2, 3], mapper: valText}), [ + '!invalid1', '!invalid2', '!#TypeError', '!#TypeError', + '', '', '', '', + '2017-07-13', '2017-07-13 4:00am', '2017-07-14', '2017-07-14 04:00:00-04:00', + '2017-05-01', '2017-05-01 7:00am', '2017-05-02', '2017-05-02 07:00:00-04:00', + '2017-04-21', '2017-04-21 12:00pm', '2017-04-22', '2017-04-22 12:00:00-04:00', + '2017-04-11', '2017-04-11 9:30am', '2017-04-12', '2017-04-12 09:30:00-04:00', + '2017-03-16', '2017-03-16 4:00pm', '2017-03-17', '2017-03-17 16:00:00-04:00', + ]); + }); + + it('should sort correctly by Any returning Date or DateTime', async function() { + // Formulas of type 'Any' returning a Date or DateTime involve comparison of complex values + // (arrays) when sorting. Check that it works even in the presence of error values. + + await clickColumnMenuSort('Order', 'Sort ascending'); + let orderRow = await $(".test-sort-config-row:contains(Order)").wait().elem(); + await assert.isPresent(orderRow); + await assert.isPresent(orderRow.find(".test-sort-config-sort-order-asc")); + await clickColumnMenuSort('CalcDate', 'Sort ascending'); + let calcDateRow = await $(".test-sort-config-row:contains(CalcDate)").wait().elem(); + await assert.isPresent(calcDateRow); + await assert.isPresent(calcDateRow.find(".test-sort-config-sort-order-asc")); + + // Check that the data is now sorted. + assert.deepEqual(await gu.getGridValues({rowNums: [1, 2, 3, 4, 5, 6, 7], cols: [0, 1, 2, 3], mapper: valText}), [ + '', '', '', '', + '2017-03-16', '2017-03-16 4:00pm', '2017-03-17', '2017-03-17 16:00:00-04:00', + '2017-04-11', '2017-04-11 9:30am', '2017-04-12', '2017-04-12 09:30:00-04:00', + '2017-04-21', '2017-04-21 12:00pm', '2017-04-22', '2017-04-22 12:00:00-04:00', + '2017-05-01', '2017-05-01 7:00am', '2017-05-02', '2017-05-02 07:00:00-04:00', + '2017-07-13', '2017-07-13 4:00am', '2017-07-14', '2017-07-14 04:00:00-04:00', + '!invalid1', '!invalid2', '!#TypeError', '!#TypeError', + ]); + + await clickColumnMenuSort('Order', 'Sort ascending'); + orderRow = await $(".test-sort-config-row:contains(Order)").wait().elem(); + await assert.isPresent(orderRow); + await assert.isPresent(orderRow.find(".test-sort-config-sort-order-asc")); + await clickColumnMenuSort('CalcDTime', 'Sort descending'); + calcDateRow = await $(".test-sort-config-row:contains(CalcDTime)").wait().elem(); + await assert.isPresent(calcDateRow); + await assert.isPresent(calcDateRow.find(".test-sort-config-sort-order-desc")); + + assert.deepEqual(await gu.getGridValues({rowNums: [1, 2, 3, 4, 5, 6, 7], cols: [0, 1, 2, 3], mapper: valText}), [ + '!invalid1', '!invalid2', '!#TypeError', '!#TypeError', + '2017-07-13', '2017-07-13 4:00am', '2017-07-14', '2017-07-14 04:00:00-04:00', + '2017-05-01', '2017-05-01 7:00am', '2017-05-02', '2017-05-02 07:00:00-04:00', + '2017-04-21', '2017-04-21 12:00pm', '2017-04-22', '2017-04-22 12:00:00-04:00', + '2017-04-11', '2017-04-11 9:30am', '2017-04-12', '2017-04-12 09:30:00-04:00', + '2017-03-16', '2017-03-16 4:00pm', '2017-03-17', '2017-03-17 16:00:00-04:00', + '', '', '', '', + ]); + }); +}); diff --git a/test/nbrowser/SortEditSave.ntest.js b/test/nbrowser/SortEditSave.ntest.js new file mode 100644 index 00000000..08cd01c0 --- /dev/null +++ b/test/nbrowser/SortEditSave.ntest.js @@ -0,0 +1,63 @@ +import { assert } from 'mocha-webdriver'; +import { $, gu, test } from 'test/nbrowser/gristUtil-nbrowser'; + +describe('SortEditSave.ntest', function() { + const cleanup = test.setupTestSuite(this); + + before(async function() { + await gu.supportOldTimeyTestCode(); + await gu.useFixtureDoc(cleanup, "Hello.grist", true); + }); + + afterEach(function() { + return gu.checkForErrors(); + }); + + it('should not jump to next row when an updated field jumps in a sorted section', async function() { + // Enter numbers and sort by them + await gu.enterGridValues(0, 1, [['1', '2', '3', '4']]); + await gu.clickCellRC(0, 1); + await gu.setType('Numeric'); + await $('.test-type-transform-apply').click(); + await gu.openColumnMenu('B'); + await $('.grist-floating-menu .test-sort-asc').click(); + + // Edit one of the numbers so that it doesn't get re-sorted. Assert that the cursor + // moves down one cell + await gu.clickCellRC(1, 1); + await gu.sendKeys("2.5", $.ENTER); + await gu.waitForServer(); + assert.equal(await $('.field_clip.has_cursor').text(), "3"); + + // Edit one of the numbers so that it gets re-sorted. Assert that the cursor stays + // on the cell + await gu.clickCellRC(1, 1); + await gu.sendKeys("3.5", $.ENTER); + await gu.waitForServer(); + assert.equal(await $('.field_clip.has_cursor').text(), "3.5"); + }); + + it('should not jump to next row when a formula update causes the field to jump', async function() { + // Enter a formula in the next column, and sort by the column + await gu.clickCellRC(0, 2); + await gu.sendKeys("="); + await $('.test-editor-tooltip-convert').click(); // Convert to a formula + await gu.sendKeys("$B", $.ENTER); + await gu.openColumnMenu('C'); + await $('.grist-floating-menu .test-sort-asc').click(); + + // Edit the formula so that the row stays in the same place. Assert that the cursor + // does NOT move down (since editing a column-wide formula, not doing data entry). + await gu.clickCellRC(0, 2); + await gu.sendKeys($.ENTER, [$.MOD, 'a'], "$B+5", $.ENTER); + await gu.waitForServer(); + assert.equal(await $('.field_clip.has_cursor').text(), "6"); + + // Edit the formula so that the row moves. Assert that the cursor says on the cell + // in this case too. + await gu.clickCellRC(0, 2); + await gu.sendKeys($.ENTER, [$.MOD, 'a'], "10-$B", $.ENTER); + await gu.waitForServer(); + assert.equal(await $('.field_clip.has_cursor').text(), "9"); + }); +}); diff --git a/test/nbrowser/Summaries.ntest.js b/test/nbrowser/Summaries.ntest.js new file mode 100644 index 00000000..db72c9ad --- /dev/null +++ b/test/nbrowser/Summaries.ntest.js @@ -0,0 +1,282 @@ +/** + * This test suite is partially duplicated as `test/nbrowser/Summaries.ts`. + */ + +import { assert } from 'mocha-webdriver'; +import { $, gu, test } from 'test/nbrowser/gristUtil-nbrowser'; + +describe('Summaries.ntest', function() { + const cleanup = test.setupTestSuite(this); + + gu.bigScreen(); + + before(async function() { + await gu.supportOldTimeyTestCode(); + await gu.useFixtureDoc(cleanup, "CC_Summaries.grist", true); + await gu.toggleSidePanel('left', 'open'); + }); + + afterEach(function() { + return gu.checkForErrors(); + }); + + it('should contain two summary tables', async function() { + // Switch to Summaries view. + await gu.actions.selectTabView('Summaries'); + await gu.getVisibleGridCells(0, [1]); + + // Check a few numbers from 'By Category' section. + assert.deepEqual(await gu.getGridValues({section: 'By Category', rowNums: [2, 10], cols: [0, 2]}), + [ 'Business Services-Mailing & Shipping', '341.84', + 'Merchandise & Supplies-Internet Purchase', '1023.47' ]); + + // Check a few numbers from 'Credit/Debit By Category' section. + assert.deepEqual(await gu.getGridValues({section: 'Credit/Debit By Category', rowNums: [3, 9], + cols: [1, 3]}), + [ 'Fees & Adjustments-Fees & Adjustments', '-472.03', + 'Business Services-Office Supplies', '526.45' ]); + assert.deepEqual(await gu.getGridValues({section: 'Credit/Debit By Category', rowNums: [3, 9], + cols: [0], mapper: e => e.find('.widget_checkmark').getCssValue('display') }), + [ 'block', + 'none' ]); + }); + + + it('should allow updating summary group-by columns', async function() { + // Open side-pane. + await gu.openSidePane('view'); + await $('.test-config-data').click(); + await gu.actions.viewSection('By Category').selectSection(); + + // Check some values in the data. + assert.deepEqual(await gu.getGridValues({rowNums: [2, 12], cols: [0, 1, 2]}), + [ 'Business Services-Mailing & Shipping', '6', '341.84', + 'Merchandise & Supplies-Pharmacies', '4', '42.19' ]); + + // Verify that multiselect only shows "Category". + assert.deepEqual(await $('.test-pwc-groupedBy-col').array().text(), ["Category"]); + + // Add another field, "Date". + await $('.test-pwc-editDataSelection').click(); + await $(`.test-wselect-column:contains(Date)`).click(); + + // Cancel, and verify contents of multiselect. + await gu.sendKeys($.ESCAPE); + assert.deepEqual(await $('.test-pwc-groupedBy-col').array().text(), ["Category"]); + + // Add another field, "Date", again. + await $('.test-pwc-editDataSelection').click(); + await $(`.test-wselect-column:contains(Date)`).click(); + + // Save, and verify contents of multiselect. + await $('.test-wselect-addBtn').click(); + await gu.waitForServer(); + + // Verify contents of multiselect. + assert.deepEqual(await $('.test-pwc-groupedBy-col').array().text(), ["Date", "Category"]); + + // Wait for data to load, and verify the data. + assert.deepEqual(await gu.getGridValues({rowNums: [2, 12], cols: [0, 1, 2, 3]}), + [ '2015-02-12', '', '1', '-4462.48', + '2015-02-13', 'Business Services-Mailing & Shipping', '1', '147.00' ]); + + // Remove both "Date" and "Category", and save. + await $('.test-pwc-editDataSelection').click(); + await $('.test-wselect-column[class*=-selected]:contains(Date)').click(); + await $('.test-wselect-column[class*=-selected]:contains(Category)').click(); + await $('.test-wselect-addBtn').click(); + await gu.waitForServer(); + + // Verify contents of multiselect. + assert.deepEqual(await $('.test-pwc-groupedBy-col').array().text(), []); + + // Wait for data to load, and verify the data (a single line of totals). + assert.deepEqual(await gu.getGridValues({rowNums: [1], cols: [0, 1]}), + ['208', '3540.60']); + + // Undo, and verify contents of multiselect. + await gu.undo(); + assert.deepEqual(await $('.test-pwc-groupedBy-col').array().text(), ["Date", "Category"]); + + // Undo, and verify contents of multiselect. + await gu.undo(); + assert.deepEqual(await $('.test-pwc-groupedBy-col').array().text(), ["Category"]); + + // Verify that contents is what we started with. + assert.deepEqual(await gu.getGridValues({rowNums: [2, 12], cols: [0, 1, 2]}), + [ 'Business Services-Mailing & Shipping', '6', '341.84', + 'Merchandise & Supplies-Pharmacies', '4', '42.19' ]); + }); + + // This test has been migrated to `test/nbrowser/Summaries.ts` + it('should allow detaching a summary table', async function() { + // Detach a summary section, make sure it shows correct data, and has live formulas, but + // doesn't auto-add rows. Then undo and make sure we go back to a summary table. + + await gu.actions.viewSection('By Category').selectSection(); + assert.deepEqual(await gu.actions.getTabs().array().text(), ['Summaries', 'Sheet1']); + + await $('.test-detach-button').click() + await gu.waitForServer(); + await assert.equal(await $(".test-pwc-groupedBy").isDisplayed(), false); + + // Verify that the title of the section has changed. + assert.equal(await $('.active_section .test-viewsection-title').parent().text(), + 'By Category'); + assert.deepEqual(await gu.actions.getTabs().array().text(), ['Summaries', 'Sheet1', 'Table1']); + + // Verify that contents of the section. + assert.deepEqual(await gu.getGridValues({rowNums: [2, 12], cols: [0, 1, 2]}), + [ 'Business Services-Mailing & Shipping', '6', '341.84', + 'Merchandise & Supplies-Pharmacies', '4', '42.19' ]); + + // See what the last row number is. + await gu.sendKeys([$.MOD, $.DOWN]); + assert.equal(await $('.active_section .gridview_data_row_num').last().text(), '19'); + + // Change a category in Transactions; it should affect formulas in existing rows of the + // detached table, but should not produce new rows. + await gu.clickCell({rowNum: 9, col: 2, section: 'Transactions'}); + await gu.sendKeys('Hello', $.ENTER); + await gu.waitForServer(); + + // Check that number of rows is unchanged, but that formulas got updated in the affected row. + await gu.actions.viewSection('By Category').selectSection(); + assert.equal(await $('.active_section .gridview_data_row_num').last().text(), '19'); + await gu.sendKeys([$.MOD, $.UP]); + assert.deepEqual(await gu.getGridValues({rowNums: [2, 12], cols: [0, 1, 2]}), + [ 'Business Services-Mailing & Shipping', '5', '194.84', + 'Merchandise & Supplies-Pharmacies', '4', '42.19' ]); + + // Undo everything. Make sure we have our summary table back. + await gu.undo(3); + assert.equal(await $('.active_section .test-viewsection-title').parent().text(), + 'By Category'); + assert.deepEqual(await $('.test-pwc-groupedBy-col').array().text(), ["Category"]); + assert.deepEqual(await gu.actions.getTabs().array().text(), ['Summaries', 'Sheet1']); + }); + + + it('should allow adding summaries by date', async function() { + // Add Summary table by Date column. + await gu.actions.viewSection('By Category').selectSection(); + await gu.actions.addNewSummarySection('Sheet1', ['Date', 'Category'], 'Table', 'By Date/Category'); + + // Check a couple of values. + await gu.actions.viewSection('By Date/Category').selectSection(); + await gu.sendKeys([$.MOD, $.DOWN]); // Go to the end. + assert.deepEqual(await gu.getGridValues({section: 'By Date/Category', rowNums:[151], cols:[0, 1, 3]}), + [ '2015-12-04', 'Travel-Lodging', '3021.54' ]); + }); + + + it('should update summary values when values change', async function() { + // Change a value in Transactions, and check that numbers changed. + await gu.actions.viewSection('Transactions').selectSection(); + await gu.getCell(1, 9).click(); + await gu.waitAppFocus(); + await gu.sendKeys('947.00', $.ENTER); // Change 147.00 -> 947.00 + assert.equal(await gu.getCell(1, 9).text(), '947.00'); + await gu.sendKeys([$.MOD, $.DOWN], $.UP); // Go to the last row (but not the "add row"). + await gu.sendKeys('677.40', $.ENTER); // Change 177.40 -> 677.40 + await gu.waitForServer(); + + // Check changes in the two affected sections. + assert.deepEqual(await gu.getGridValues({section: 'By Category', rowNums: [2, 10], cols: [0, 2]}), + [ 'Business Services-Mailing & Shipping', '1141.84', // <--- this changes + 'Merchandise & Supplies-Internet Purchase', '1023.47' ]); + assert.deepEqual(await gu.getGridValues({section: 'By Date/Category', rowNums:[151], cols:[0, 1, 3]}), + [ '2015-12-04', 'Travel-Lodging', '3521.54' ]); + + // Undo both changes, and check that summarized values got restored. + await $(".test-undo").click(); + await $(".test-undo").click(); + await gu.waitForServer(); + + assert.deepEqual(await gu.getGridValues({section: 'By Category', rowNums: [2, 10], cols: [0, 2]}), + [ 'Business Services-Mailing & Shipping', '341.84', + 'Merchandise & Supplies-Internet Purchase', '1023.47' ]); + assert.deepEqual(await gu.getGridValues({section: 'By Date/Category', rowNums:[151], cols:[0, 1, 3]}), + [ '2015-12-04', 'Travel-Lodging', '3021.54' ]); + }); + + + it('should update summary values when key columns change', async function() { + // Change a category in Transactions, and check that numbers changed. + await gu.actions.viewSection('Transactions').selectSection(); + await gu.sendKeys([$.MOD, $.DOWN]); // Go to the end. + await gu.getCell(2, 208).click(); + await gu.waitAppFocus(); + await gu.sendKeys('Merchandise & Supplies-Internet Purchase', $.ENTER); + assert.equal(await gu.getCell(2, 208).text(), 'Merchandise & Supplies-Internet Purchase'); + await gu.waitForServer(); + + // Check that numbers changed in two affected summary tables. + assert.deepEqual(await gu.getGridValues({section: 'By Category', rowNums: [2, 10], cols: [0, 2]}), + [ 'Business Services-Mailing & Shipping', '341.84', + 'Merchandise & Supplies-Internet Purchase', '1200.87' ]); // Up by 177.40 + assert.deepEqual(await gu.getGridValues({section: 'By Date/Category', + rowNums:[151, 152], cols:[0, 1, 3]}), + [ '2015-12-04', 'Travel-Lodging', '2844.14', // Down by 177.40 + '2015-12-04', 'Merchandise & Supplies-Internet Purchase', '177.40' ]); // New row + + // Undo and check that summarized values got restored. + await $(".test-undo").click(); + await gu.waitForServer(); + + assert.deepEqual(await gu.getGridValues({section: 'By Category', rowNums: [2, 10], cols: [0, 2]}), + [ 'Business Services-Mailing & Shipping', '341.84', + 'Merchandise & Supplies-Internet Purchase', '1023.47' ]); + assert.deepEqual(await gu.getGridValues({section: 'By Date/Category', rowNums:[151], cols:[0, 1, 3]}), + [ '2015-12-04', 'Travel-Lodging', '3021.54' ]); + + // Check that the newly-added row is gone. + await gu.actions.viewSection('By Date/Category').selectSection(); + await assert.equal(await gu.getGridLastRowText(), '151'); + }); + + + it('should update summary values when records get added', async function() { + // Add a record. + await gu.actions.viewSection('Transactions').selectSection(); + await gu.addRecord(['2016-01-01', '100', 'Business Services-Office Supplies']); + await gu.waitForServer(); + assert.equal(await gu.getGridLastRowText(), '210'); + assert.deepEqual(await gu.getGridValues({cols: [0, 1, 2], rowNums: [209]}), + ['2016-01-01', '100.00', 'Business Services-Office Supplies']); + + // Check that numbers have changed. + assert.deepEqual(await gu.getGridValues({section: 'Credit/Debit By Category', rowNums: [2, 3, 9], + cols: [1, 3]}), + [ 'Business Services-Office Supplies', '-4.56', // <-- no change + 'Fees & Adjustments-Fees & Adjustments', '-472.03', + 'Business Services-Office Supplies', '626.45' ]); // <-- does change + assert.deepEqual(await gu.getGridValues({section: 'Credit/Debit By Category', rowNums: [2, 3, 9], + cols: [0], mapper: e => e.find('.widget_checkmark').getCssValue('display')}), + [ 'block', + 'block', + 'none']); // <-- does change + + // Go to last data record. + await gu.sendKeys([$.MOD, $.UP]); + await gu.sendKeys([$.MOD, $.DOWN]); + await gu.sendKeys([$.UP]); + + // Delete the new record, and check that values are the same as before. + await gu.sendKeys([$.MOD, $.DELETE]); + + await gu.confirm(true, true); // confirm and remember. + await gu.waitForServer(); + + assert.deepEqual(await gu.getGridValues({section: 'Credit/Debit By Category', rowNums: [2, 3, 9], + cols: [1, 3]}), + [ 'Business Services-Office Supplies', '-4.56', + 'Fees & Adjustments-Fees & Adjustments', '-472.03', + 'Business Services-Office Supplies', '526.45' ]); + assert.deepEqual(await gu.getGridValues({section: 'Credit/Debit By Category', rowNums: [2, 3, 9], + cols: [0], mapper: e => e.find('.widget_checkmark').getCssValue('display')}), + [ 'block', + 'block', + 'none' ]); + }); +}); diff --git a/test/nbrowser/TextEditor.ntest.js b/test/nbrowser/TextEditor.ntest.js new file mode 100644 index 00000000..a6ab36db --- /dev/null +++ b/test/nbrowser/TextEditor.ntest.js @@ -0,0 +1,280 @@ +import { assert, driver } from 'mocha-webdriver'; +import { $, gu, test } from 'test/nbrowser/gristUtil-nbrowser'; + +describe('TextEditor.ntest', function() { + test.setupTestSuite(this); + before(async function() { + await gu.supportOldTimeyTestCode(); + await gu.actions.createNewDoc(); + }); + + afterEach(function() { + return gu.checkForErrors(); + }); + + async function autoCompleteSelect(options) { + await gu.sendKeys(options.input); + const values = await $('.test-ref-editor-item').array().text(); + if (options.keys) { + await gu.sendKeys(...options.keys); + await $('.test-ref-editor-item.selected').wait(assert.isPresent, true); + } else if (options.click) { + await driver.findContent('.test-ref-editor-item', gu.exactMatch(options.click)).click(); + } + return values; + } + + async function autoCompleteWaitForSelection(text, selected) { + await $('.test-ref-editor-item:contains('+ text +')').wait(assert.hasClass, 'selected', selected); + } + + it('should allow saving values into new Reference column', async function() { + await gu.getCellRC(0, 0).wait().click(); + await gu.sendKeys("foo", $.ENTER); + await gu.waitForServer(); + await gu.sendKeys("bar", $.ENTER); + await gu.waitForServer(); + await gu.sendKeys("baz", $.ENTER); + await gu.waitForServer(); + + // Add a new section and switch to it. + await gu.actions.addNewSection('New', 'Table'); + await gu.toggleSidePanel('left', 'close'); + await $(".viewsection_title:contains(TABLE2)").click(); + await gu.getCellRC(0, 0).click(); + await gu.setType('Reference'); + await gu.setRefTable('Table1'); + await gu.setVisibleCol('A'); + + // Populate some of the reference column. + await gu.getCellRC(0, 0).click(); + + // Select "foo" from autocomplete dropdown with keyboard. + await autoCompleteSelect({input: 'f'}); + await gu.sendKeys($.ENTER); + await gu.waitForServer(); + assert.equal(await gu.getCellRC(0, 0).text(), "foo"); + + // Select "bar" from autocomplete dropdown with the mouse. + await autoCompleteSelect({input: 'b', click: 'bar'}); + await gu.waitForServer(); + await gu.sendKeys($.DOWN); // Selecting with the mouse saves without moving the cursor + assert.equal(await gu.getCellRC(1, 0).text(), "bar"); + + // Entering an existing value should reference it + await autoCompleteSelect({input: 'baz'}); + await gu.sendKeys($.ENTER); + await gu.waitForServer(); + assert.equal(await gu.getCellRC(2, 0).text(), "baz"); + + // Select "foo" from autocomplete dropdown with tab. + await autoCompleteSelect({input: 'foo'}); + await gu.sendKeys($.TAB); // Select "foo" from autocomplete dropdown with tab. + await gu.waitForServer(); + assert.equal(await gu.getCellRC(3, 0).text(), "foo"); + + // Esc should Cancel. + await gu.getCellRC(4, 0).click(); + await autoCompleteSelect({input: 'baz'}); + await gu.sendKeys($.ESCAPE); + assert.equal(await gu.getCellRC(4, 0).text(), ""); + }); + + it('should allow adding new values from Reference column', async function() { + // Select add new from autocomplete dropdown. + await autoCompleteSelect({input: 'foobar', keys: [$.UP]}); + await gu.sendKeys($.ENTER); + await gu.waitForServer(); + await $(".viewsection_title:contains(TABLE1)").click(); + assert.equal(await gu.getCellRC(3, 0).text(), "foobar"); + + // Add new by tab + await $(".viewsection_title:contains(TABLE2)").click(); + await gu.getCellRC(4, 0).click(); + await autoCompleteSelect({input: 'foobar1', keys: [$.UP]}); + await gu.sendKeys($.TAB); + await gu.waitForServer(); + await $(".viewsection_title:contains(TABLE1)").click(); + assert.equal(await gu.getCellRC(4, 0).text(), "foobar1"); + + // Add new by click + await $(".viewsection_title:contains(TABLE2)").click(); + await gu.getCellRC(5, 0).click(); + await autoCompleteSelect({input: 'foobar2', click: 'foobar2'}); + await gu.waitForServer(); + await $(".viewsection_title:contains(TABLE1)").click(); + assert.equal(await gu.getCellRC(5, 0).text(), "foobar2"); + + // Cancel with escape + await $(".viewsection_title:contains(TABLE2)").click(); + await gu.getCellRC(5, 0).click(); + await autoCompleteSelect({input: 'foobar3', keys: [$.UP]}); + await gu.sendKeys($.ESCAPE); + await gu.waitForServer(); + await gu.waitAppFocus(true); + await $(".viewsection_title:contains(TABLE1)").click(); + assert.equal(await gu.getCellRC(6, 0).text(), ""); + + // Once add new is selected it should not be possible to change the input. + await $(".viewsection_title:contains(TABLE2)").click(); + await gu.getCellRC(6, 0).click(); + await autoCompleteSelect({input: 'foobar4', keys: [$.UP]}); + await gu.sendKeys("567"); + // Make sure add item loses selection + await autoCompleteWaitForSelection('foobar4', false); + await gu.sendKeys($.ENTER); + await gu.waitForServer(); + assert.equal(await gu.getCellRC(6, 0).text(), "foobar4567"); + await assert.hasClass(gu.getCellRC(6, 0).find('.field_clip'), 'invalid'); + await $(".viewsection_title:contains(TABLE1)").click(); + assert.equal(await gu.getCellRC(6, 0).text(), ""); + }); + + async function addColumnRightOf(index) { + // Add a column. We have to hover over the column header first. + await gu.openColumnMenu({col: index}, 'Insert column to the right'); + await gu.waitForServer(); + await gu.sendKeys($.ESCAPE); + } + + it('should allow saving values into new Date column', async function() { + // Add another column. We have to hover over the column header first. + await addColumnRightOf(0); + await gu.getCellRC(0, 1).click(); + + // Convert to Date. No need to "Apply conversion" since it's a new empty column. + await gu.setType('Date'); + + // Enter a new value and check that it's parsed and shows correctly. + await gu.getCellRC(0, 1).click(); + await gu.sendKeys("2016/04/20", $.ENTER); + await gu.waitForServer(); + assert.equal(await gu.getCellRC(0, 1).text(), "2016-04-20"); + }); + + it('should show formatted values for ReferenceEditor autocomplete', async function() { + // Set a Reference column to use a displayCol that's a Date, and ensure that properly + // formatted dates show in its autocomplete. + + // First, fill in a few more dates into Table1.D + await gu.enterGridValues(1, 1, [['2014-03-14', '2017-05-01', '2016-12-31', '', '2011-07-15']]); + + // Now switch to the section with the Reference column and switch its displayCol to Table1.D. + await gu.actions.viewSection('TABLE2').selectSection(); + await gu.clickCell({rowNum: 1, col: 0}); + await gu.setVisibleCol('D'); + + // Check that the values displayed are properly formatted. + assert.deepEqual(await gu.getGridValues({rowNums: [1, 2, 3, 4, 5, 6, 7], cols: [0]}), + ['2016-04-20', '2014-03-14', '2017-05-01', '2016-04-20', '[Blank]', '2011-07-15', 'foobar4567']); + + // Check that formatted values are shown in the auto-complete dropdown. + await gu.clickCell({rowNum: 3, col: 0}); + assert.deepEqual(await autoCompleteSelect({input: '2016', keys: [$.DOWN]}), + ['2016-04-20', '2016-12-31', '2011-07-15', '2014-03-14', '2017-05-01', '2016']); + await gu.sendKeys($.ENTER); + await gu.waitForServer(); + + // Check that after selection, the right value is saved, and that it's valid (not AltText). + let cell = await gu.getCell({rowNum: 3, col: 0}); + assert.equal(await cell.text(), '2016-12-31'); + await assert.hasClass(cell.find('.field_clip'), 'invalid', false); + + // Check that the formatted value is used to start the autocomplete lookup. + await gu.clickCell({rowNum: 3, col: 0}); + assert.deepEqual(await autoCompleteSelect({input: $.ENTER}), + ['2016-12-31', '2016-04-20', '2011-07-15', '2014-03-14', '2017-05-01']); + await gu.sendKeys($.SELECT_ALL, '2017-05-01', $.ENTER); + await gu.waitForServer(); + + // Check that after typing, the right value is saved, and that it's valid (not AltText). + cell = await gu.getCell({rowNum: 3, col: 0}); + assert.equal(await cell.text(), '2017-05-01'); + await assert.hasClass(cell.find('.field_clip'), 'invalid', false); + + // Switch back to the view section we started from. + await gu.actions.viewSection('TABLE1').selectSection(); + }); + + + it('should allow saving values into new Checkbox column', async function() { + await addColumnRightOf(1); + await gu.getCellRC(0, 2).click(); + + // Convert to Toggle. No need to "Apply conversion" since it's a new empty column. + await gu.setType('Toggle'); + + // Toggle a value in the new column. + await gu.getCellRC(1, 2).find('.widget_checkbox').click(); + await gu.waitForServer(); + + // To ensure it got saved to the server, convert to text, and check the text. + await gu.setType('Text'); + await $('.test-type-transform-apply').wait().click(); + await gu.waitForServer(); + assert.deepEqual(await gu.getVisibleGridCells(2, [1, 2, 3]), + ["false", "true", "false"]); + }); + + it('should allow saving values into a new row of a new column', async function() { + await gu.getColumnHeader('A').scrollIntoView({inline: "end"}); + await addColumnRightOf(0); + await gu.getCellRC(0, 1).click(); + await gu.setType('Date'); + + assert.equal(await gu.getCellRC(6, 1).text(), ""); // Last "add new" row. + await assert.isPresent(gu.getCellRC(7, 1), false); // Check that there is no next row. + + await gu.getCellRC(0, 1).click(); + await gu.sendKeys([$.MOD, $.DOWN]); // Jump to last row. + await gu.sendKeys("2001/11/23", $.ENTER); + await gu.waitForServer(); + + assert.equal(await gu.getCellRC(6, 1).text(), "2001-11-23"); + await assert.isPresent(gu.getCellRC(7, 1), true); // Check that there is now one more row. + }); + + it('should allow changing a Date column to/from formula', async function() { + // What column D (index 1) start off with. + assert.equal(await gu.getCellRC(0, 1).text(), ""); + assert.equal(await gu.getCellRC(6, 1).text(), "2001-11-23"); + + // Replace it with a formula that uses another date column B. + await gu.getCellRC(0, 1).click(); + await gu.sendKeys('='); + await $('.test-editor-tooltip-convert').click(); // Convert to a formula + await gu.sendKeys('$D and $D.replace(day=2)', $.ENTER); + await gu.waitForServer(); + + // Check that it worked. + assert.equal(await gu.getCellRC(0, 1).text(), "2016-04-02"); + assert.equal(await gu.getCellRC(6, 1).text(), ""); + + // Converting it to a data column. + await gu.clickColumnMenuItem('F', 'Convert formula to data'); + await gu.waitForServer(); + assert.equal(await gu.getCellRC(0, 1).text(), "2016-04-02"); + assert.equal(await gu.getCellRC(6, 1).text(), ""); + + // Enter a new value, make sure that works. + await gu.getCellRC(6, 1).click(); + await gu.sendKeys("2016/05/01", $.ENTER); + await gu.waitForServer(); + assert.equal(await gu.getCellRC(0, 1).text(), "2016-04-02"); + assert.equal(await gu.getCellRC(6, 1).text(), "2016-05-01"); + }); + + // NOTE: This tests a specific bug which prevented moving the editor cursor via clicking. + // See https://phab.getgrist.com/T326 + it('should allow moving cursor inside the editor via clicking', async function() { + await gu.clickCellRC(0, 0); + await gu.sendKeys($.ENTER); + await gu.waitAppFocus(false); + // Double click the cell to select all the text. This will fail if the bug is active. + await driver.withActions(a => a.doubleClick($('.celleditor_text_editor').elem())); + // Since the text was selected, the new text will replace the old text. + await gu.sendKeys('abcd', $.ENTER); + await gu.waitForServer(); + assert.equal(await gu.getCellRC(0, 0).text(), "abcd"); + }); +}); diff --git a/test/nbrowser/TypeChange.ntest.js b/test/nbrowser/TypeChange.ntest.js new file mode 100644 index 00000000..9819e13e --- /dev/null +++ b/test/nbrowser/TypeChange.ntest.js @@ -0,0 +1,463 @@ +import { assert } from 'mocha-webdriver'; +import { $, gu, test } from 'test/nbrowser/gristUtil-nbrowser'; + +// Helper that returns the cell text prefixed by "!" if the cell is invalid. +async function valText(cell) { + const isInvalid = await cell.find('.field_clip').hasClass("invalid"); + const text = await cell.getText(); + return (isInvalid ? "!" : "") + text; +} + +describe('TypeChange.ntest', function() { + const cleanup = test.setupTestSuite(this); + + before(async function() { + await gu.supportOldTimeyTestCode(); + await gu.useFixtureDoc(cleanup, "Hello.grist", true); + }); + + afterEach(function() { + return gu.checkForErrors(); + }); + + it('should not use transform to convert type for an empty column', async function() { + await gu.openSidePane('field'); + await gu.getCellRC(0, 0).click(); + await gu.sendKeys([$.ALT, '=']); + await gu.waitForServer(); + gu.sendKeys($.ESCAPE); + + // Click on new column + await gu.getCellRC(0, 1).click(); + + // Change type + await gu.userActionsCollect(); + await gu.setType('Numeric'); + await gu.userActionsVerify([["UpdateRecord", "_grist_Tables_column", 7, {"type": "Numeric"}]]); + + // Errors should not be present in converted column + assert.isFalse(await gu.getCellRC(0, 1).find('.field_clip').hasClass('invalid')); + + // Ensure that column transform is not occurring. + assert.isFalse(await $('.type_transform_prompt').isPresent()); + }); + + it('should use transform to convert type for non-empty columns', async function() { + // Enter text into numeric column + await gu.getCellRC(0, 1).click(); + await gu.sendKeys('one', $.ENTER); + await gu.waitForServer(); + assert.hasClass(await gu.getCellRC(0, 1).find('.field_clip'), 'invalid', true); + + // Change numeric to text + await gu.userActionsCollect(); + await gu.setType('Text'); + + // Accept, check that column is text and has no errors + await gu.applyTypeConversion(); + assert.hasClass(await gu.getCellRC(0, 1).find('.field_clip'), 'invalid', false); + await gu.userActionsVerify([ + ["AddColumn", "Table1", "gristHelper_Converted", { type: 'Any' }], + ["AddColumn", "Table1", "gristHelper_Transform", { type: 'Any' }], + ["ModifyColumn", "Table1", "gristHelper_Converted", { + "formula": "", + "isFormula": false, + "type": "Text", + "visibleCol": 0 + }], + ["ModifyColumn", "Table1", "gristHelper_Transform", { + "formula": "rec.gristHelper_Converted", + "isFormula": true, + "type": "Text", + "visibleCol": 0 + }], + ["ConvertFromColumn", "Table1", "F", "gristHelper_Converted", "Text", "", 0], + + // Repeated conversion just before applying + ["ConvertFromColumn", "Table1", "F", "gristHelper_Converted", "Text", "", 0], + + ["CopyFromColumn", "Table1", "gristHelper_Transform", "F", + "{\"widget\":\"TextBox\",\"alignment\":\"left\"}"], + ["RemoveColumn", "Table1", "gristHelper_Transform"], + ["RemoveColumn", "Table1", "gristHelper_Converted"], + ]); + + // Check that selected reads text + await gu.assertType('Text'); + }); + + it('should allow cancelling type changes', async function() { + // Enter bools into text column + await gu.getCellRC(0, 1).click(); + await gu.sendKeys('false', $.ENTER); + await gu.getCellRC(1, 1).click(); + await gu.sendKeys('true', $.ENTER); + + // Change text to bool + await gu.setType('Toggle'); + + // Check that column appears bool during transform + assert.isDisplayed(await gu.getCellRC(1, 1).find('.widget_checkmark').wait(), true); + assert.isDisplayed(await gu.getCellRC(0, 1).find('.widget_checkmark'), false); + assert.hasClass(await gu.getCellRC(0, 1).find('.field_clip'), 'invalid', false); + + // Cancel transform, check that column is still text + await $('.test-type-transform-cancel').wait().click(); + assert.equal(await gu.getCellRC(0, 1).find('.field_clip').text(), 'false'); + + // Check that selected reads text + await gu.assertType('Text'); + }); + + it('should allow revising type changes', async function() { + // Change text to integer + await gu.setType('Integer'); + + // Revise formula to get text length and accept + await $('.test-type-transform-revise').wait().click(); + await $('.test-type-transform-formula').click(); + await gu.sendKeys($.SELECT_ALL, $.DELETE, 'return len($F) + 1'); + + // Check that updating the type conversion works + await $('.test-type-transform-update').click(); + await gu.waitForServer(); + assert.equal(await gu.getCellRC(0, 1).find('.field_clip').text(), '6'); + + // Check that applying the type conversion without first updating works + // (the weird formula keeps other tests consistent with past behaviour) + await $('.test-type-transform-formula').click(); + await gu.sendKeys($.SELECT_ALL, $.DELETE, 'return len($F.replace("0", "0.0"))'); + await gu.waitForServer(); + await gu.applyTypeConversion(); + + // Check that column is integer and has no errors + assert.equal(await gu.getCellRC(0, 1).find('.field_clip').text(), '5'); + assert.isFalse(await gu.getCellRC(0, 1).find('.field_clip').hasClass('invalid')); + }); + + it('should allow configuring reference changes', async function() { + // Prepare new table and section + await gu.actions.addNewSection('New', 'Table'); + await gu.waitForServer(); + await $('.test-viewlayout-section-4').click(); + await gu.addRecord(['green']); + await gu.addRecord(['blue']); + + // Change type to reference column + await gu.actions.viewSection('Table1').selectSection(); + await gu.getCellRC(0, 3).click(); + await gu.waitAppFocus(true); + await gu.sendKeys('blue', $.ENTER); + await gu.getCellRC(1, 3).click(); + await gu.sendKeys('green', $.ENTER); + await gu.waitForServer(); + await gu.userActionsCollect(); + await gu.setType('Reference'); + + // Assert the correct column is selected and that the formula matches the selected + assert.equal(await $('.test-fbuilder-ref-table-select .test-select-row').getText(), 'Table2'); + assert.equal(await $('.test-fbuilder-ref-col-select .test-select-row').getText(), 'A'); + await $('.test-type-transform-revise').click(); + var aceText = await gu.getAceText($('.test-type-transform-formula').elem()); + assert.equal(aceText, "rec.gristHelper_Converted"); + + // Apply transform and check that field is a reference + await gu.applyTypeConversion(); + await gu.userActionsVerify([ + ["AddColumn", "Table1", "gristHelper_Converted", { type: 'Any' }], + ["AddColumn", "Table1", "gristHelper_Transform", { type: 'Any' }], + ["ModifyColumn", "Table1", "gristHelper_Converted", { + "formula": "", + "isFormula": false, + "type": "Ref:Table2", + "visibleCol": 9 + }], + ["ModifyColumn", "Table1", "gristHelper_Transform", { + "formula": "rec.gristHelper_Converted", + "isFormula": true, + "type": "Ref:Table2", + "visibleCol": 9 + }], + ["ConvertFromColumn", "Table1", "C", "gristHelper_Converted", "Ref:Table2", "", 9], + // Set display formula for transform column. + ["SetDisplayFormula", "Table1", null, 13, "$gristHelper_Transform.A"], + + // Repeated conversion just before applying + ["ConvertFromColumn", "Table1", "C", "gristHelper_Converted", "Ref:Table2", "", 9], + + ["CopyFromColumn", "Table1", "gristHelper_Transform", "C", "{\"widget\":\"Reference\",\"alignment\":\"left\"}"], + // We used to unset field display formula, but we don't actually use it during transforms. + ["RemoveColumn", "Table1", "gristHelper_Transform"], + ["RemoveColumn", "Table1", "gristHelper_Converted"], + ]); + + assert.hasClass(await gu.getCellRC(0, 3).find('.field_clip div'), 'test-ref-link-icon'); + + // Check conversion back to text + await gu.setType('Text'); + await $('.test-type-transform-revise').click(); + aceText = await gu.getAceText($('.test-type-transform-formula').elem()); + assert.equal(aceText, 'rec.gristHelper_Converted'); + await gu.applyTypeConversion(); + assert.equal(await gu.getCellRC(0, 3).find('.field_clip').getText(), 'blue'); + }); + + it('should allow configuring date and datetime changes', async function() { + await gu.toggleSidePanel("left", "close"); + await gu.getCellRC(0, 2).scrollIntoView({inline: "end"}).click(); + await gu.sendKeys('4/2/93', $.ENTER); + await gu.getCellRC(1, 2).click(); + await gu.sendKeys('4/26/16', $.ENTER); + + // Convert to Date + await gu.setType('Date'); + // Guessed date format M/D/YY + assert.equal(await gu.dateFormat(), 'Custom'); + assert.equal(await $('$Widget_dateCustomFormat input').val(), 'M/D/YY'); + // Change manually to a more formal date format + await gu.dateFormat('MM/DD/YYYY'); + assert.equal(await gu.dateFormat(), 'MM/DD/YYYY'); + await gu.waitForServer(); + // Check formula + await $('.test-type-transform-revise').wait().click(); + var aceText = await gu.getAceText($('.test-type-transform-formula').elem()); + assert.equal(aceText, "rec.gristHelper_Converted"); + + // Apply transform and check that field has correct value + await gu.applyTypeConversion(); + assert.deepEqual(await gu.getGridValues({rowNums: [1, 2], cols: [2], mapper: valText}), [ + '04/02/1993', '04/26/2016' + ]); + + // Convert back to text + await gu.setType('Text'); + await gu.applyTypeConversion(); + assert.deepEqual(await gu.getGridValues({rowNums: [1, 2], cols: [2], mapper: valText}), [ + '04/02/1993', '04/26/2016' + ]); + + await gu.getCellRC(0, 2).click(); + await gu.sendKeys($.ENTER, ' 12:00am'); + await gu.getCellRC(1, 2).click(); + await gu.sendKeys($.ENTER, ' 4:00am', $.ENTER); + + // Convert to DateTime and assert formula matches options + await gu.setType('DateTime'); + await $('.test-tz-autocomplete').click(); + await gu.sendKeys('Los_Ang', $.ENTER); + await gu.waitForServer(); + assert.equal(await $('.test-tz-autocomplete input').val(), 'America/Los_Angeles'); + assert.equal(await gu.dateFormat(), 'MM/DD/YYYY'); + assert.equal(await gu.timeFormat(), 'h:mma'); + await $('.test-type-transform-revise').click(); + aceText = await gu.getAceText($('.test-type-transform-formula').elem()); + assert.equal(aceText, "rec.gristHelper_Converted"); + + // Apply transform and check that field has correct value + await gu.applyTypeConversion(); + assert.deepEqual(await gu.getGridValues({rowNums: [1, 2], cols: [2], mapper: valText}), [ + '04/02/1993 12:00am', '04/26/2016 4:00am', + ]); + assert.equal(await $('.test-tz-autocomplete input').val(), 'America/Los_Angeles'); + + // Convert DateTime to Date and check that we are getting the right date. + await gu.setType('Date'); + await gu.applyTypeConversion(); + assert.deepEqual(await gu.getGridValues({rowNums: [1, 2], cols: [2], mapper: valText}), [ + '04/02/1993', '04/26/2016' + ]); + + // Convert Date to DateTime and check that we are getting midnight in selected timezone. + await gu.setType('DateTime'); + await gu.timeFormat('HH:mm z'); + await gu.waitForServer(); + assert.deepEqual(await gu.getGridValues({rowNums: [1, 2], cols: [2], mapper: valText}), [ + '04/02/1993 00:00 EST', '04/26/2016 00:00 EDT' + ]); + await $('.test-tz-autocomplete').click(); + await gu.sendKeys('Los_Ang', $.ENTER); + await gu.waitForServer(); + assert.equal(await $('.test-tz-autocomplete input').val(), 'America/Los_Angeles'); + assert.equal(await gu.dateFormat(), 'MM/DD/YYYY'); + assert.equal(await gu.timeFormat(), 'HH:mm z'); + await gu.waitForServer(); + await gu.applyTypeConversion(); + assert.deepEqual(await gu.getGridValues({rowNums: [1, 2], cols: [2], mapper: valText}), [ + '04/02/1993 00:00 PST', '04/26/2016 00:00 PDT' + ]); + }); + + it('should trigger a transform when reference table is changed', async function() { + // Set up conditions for the test + await gu.getSection('Table1').click(); + await gu.enterGridValues(2, 3, [['red', 'yellow']]); + await gu.actions.addNewSection('New', 'Table'); + await gu.getSection('TABLE3').click(); + await gu.enterGridValues(0, 1, [['yellow', 'red', 'green', 'blue']]); + await gu.getSection('Table1').click(); + await gu.clickCellRC(0, 3); + await gu.openSidePane('field'); + await gu.setType('Reference'); + await gu.setRefTable('Table2'); + await gu.waitForServer(); + await gu.setVisibleCol('A'); + await gu.applyTypeConversion(); + assert.deepEqual(await gu.getGridValues({rowNums: [1, 2, 3, 4], cols: [3], mapper: valText}), [ + 'blue', 'green', '!red', '!yellow' + ]); + + // Check that row ids shows 2, 1, (AltText), (AltText) + await gu.setVisibleCol('Row ID'); + assert.deepEqual(await gu.getGridValues({rowNums: [1, 2, 3, 4], cols: [3]}), + ['Table2[2]', 'Table2[1]', 'red', 'yellow']); + await gu.setVisibleCol('A'); + + // Should trigger the transform + await gu.setRefTable('Table3'); + await gu.waitForServer(); + await gu.setVisibleCol('B'); + await assert.isPresent($('.type_transform_prompt')); + + // Transform should follow the format Ref: -> Text -> Ref: + await gu.applyTypeConversion(); + assert.deepEqual(await gu.getGridValues({rowNums: [1, 2, 3, 4], cols: [3]}), + ['blue', 'green', 'red', 'yellow']); + // Check that the cells are no longer invalid. + assert.deepEqual(await gu.getGridValues({rowNums: [1, 2, 3, 4], cols: [3], mapper: valText}), [ + 'blue', 'green', 'red', 'yellow' + ]); + + // Check that row ids have changed, despite text remaining the same. + await gu.setVisibleCol('Row ID'); + assert.deepEqual(await gu.getGridValues({rowNums: [1, 2, 3, 4], cols: [3]}), + ['Table3[4]', 'Table3[3]', 'Table3[2]', 'Table3[1]']); + }); + + it('should allow undoing a reference transform in one step', async function() { + await gu.setVisibleCol('B'); + await gu.setType('Text'); + await gu.applyTypeConversion(); + await gu.setType('Reference'); + await gu.applyTypeConversion(); + await gu.undo(); + // Undoing once should return the column to Text with the correct values. + await gu.assertType('Text'); + assert.deepEqual(await gu.getGridValues({rowNums: [1, 2, 3, 4], cols: [3]}), + ['blue', 'green', 'red', 'yellow']); + }); + + it('should cancel an in-progress transformation on undo', async function() { + assert.deepEqual(await gu.getGridValues({rowNums: [1, 2, 3, 4], cols: [3]}), + ['blue', 'green', 'red', 'yellow']); + await gu.setType('Reference'); + await gu.assertType('Reference'); + await assert.isPresent($('.test-type-transform-top'), true); + await gu.undo(); + await assert.isPresent($('.test-type-transform-top'), false); + await gu.assertType('Text'); + assert.deepEqual(await gu.getGridValues({rowNums: [1, 2, 3, 4], cols: [3]}), + ['blue', 'green', 'red', 'yellow']); + }); + + // NOTE: This tests a bug fix where integer values not present in the reference + // column were mistaken for row ids and converted to row values instead of AltText values. + it('should properly convert from integer to reference', async function() { + // Set up conditions for the test + await gu.getSection('TABLE3').click(); + await gu.enterGridValues(0, 2, [['3', '3', '4', '1']]); + await gu.waitForServer(); + await gu.setType('Integer'); + await gu.applyTypeConversion(); + + // Begin convert to reference. + await gu.setType('Reference'); + await gu.assertType('Reference'); + await assert.isPresent($('.test-type-transform-top'), true); + + // Convert to a reference and check that the values are valid and as expected + // before and after the conversion. The last row should be invalid since there + // is no matching record in the destination col. + assert.deepEqual(await gu.getGridValues({rowNums: [1, 2, 3, 4], cols: [2], mapper: valText}), [ + '3', '3', '4', '!1' + ]); + await gu.applyTypeConversion(); + assert.deepEqual(await gu.getGridValues({rowNums: [1, 2, 3, 4], cols: [2], mapper: valText}), [ + '3', '3', '4', '!1' + ]); + }); + + // NOTE: This tests a bug fix where reference transforms to numeric types gave + // error values by default. + it('should properly convert from reference to integer/numeric', async function() { + await gu.clickCellRC(0, 2); + + // Convert to an integer and check that the values are valid and as expected before + // and after the conversion. This ensures that AltText values can be cast back into ints. + await gu.setType('Integer'); + assert.deepEqual(await gu.getGridValues({rowNums: [1, 2, 3, 4], cols: [2], mapper: valText}), [ + '3', '3', '4', '1' + ]); + await gu.applyTypeConversion(); + assert.deepEqual(await gu.getGridValues({rowNums: [1, 2, 3, 4], cols: [2], mapper: valText}), [ + '3', '3', '4', '1' + ]); + + // Switch back to a reference column + await gu.setType('Reference'); + await gu.assertType('Reference'); + await assert.isPresent($('.test-type-transform-top'), true); + await gu.applyTypeConversion(); + + // Convert to numeric and check the values are valid and as expected. + // This ensures that AltText values can be cast back into floats. + await gu.setType('Numeric'); + assert.deepEqual(await gu.getGridValues({rowNums: [1, 2, 3, 4], cols: [2], mapper: valText}), [ + '3', '3', '4', '1' + ]); + await gu.applyTypeConversion(); + assert.deepEqual(await gu.getGridValues({rowNums: [1, 2, 3, 4], cols: [2], mapper: valText}), [ + '3', '3', '4', '1' + ]); + }); + + // NOTE: This tests a bug fix where numeric types were not properly converted to + // boolean values. + it('should properly convert from integer/numeric to boolean', async function() { + // Update the Numeric column to include some falsy/truthy numbers and alttext. + await gu.clickCellRC(0, 2); + await gu.sendKeys('0'); + await gu.clickCellRC(4, 2); + await gu.sendKeys('False', $.ENTER); + await gu.waitForServer(); + await gu.sendKeys('true', $.ENTER); + await gu.waitForServer(); + + // Assert that the values are set up properly. + assert.deepEqual(await gu.getGridValues({rowNums: [1, 2, 3, 4, 5, 6], cols: [2], mapper: valText}), [ + '0', '3', '4', '1', '!False', '!true' + ]); + + // Convert the column to boolean. Assert all the values are valid and as expected. + await gu.setType('Toggle'); + await gu.applyTypeConversion(); + await gu.setWidget('TextBox'); + + // Check that the values have transformed without errors, and are as expected. + assert.deepEqual(await gu.getGridValues({rowNums: [1, 2, 3, 4, 5, 6], cols: [2], mapper: valText}), [ + 'false', '!3', '!4', 'true', 'false', 'true' + ]); + // Check that sorting by the column has the expected effect. + await gu.openColumnMenu('C'); + await $(`.grist-floating-menu .test-sort-asc`).click(); + assert.deepEqual(await gu.getGridValues({rowNums: [1, 2, 3, 4, 5, 6], cols: [2], mapper: valText}), [ + 'false', 'false', 'true', 'true', '!3', '!4' + ]); + + // Undo the widget option and type conversion and assert that the values are properly restored. + // (but still sorted) + await gu.undo(2); + assert.deepEqual(await gu.getGridValues({rowNums: [1, 2, 3, 4, 5, 6], cols: [2], mapper: valText}), [ + '0', '1', '3', '4', '!False', '!true' + ]); + }); +}); diff --git a/test/nbrowser/UndoJumps.ntest.js b/test/nbrowser/UndoJumps.ntest.js new file mode 100644 index 00000000..87f1cdc3 --- /dev/null +++ b/test/nbrowser/UndoJumps.ntest.js @@ -0,0 +1,160 @@ +import { assert } from 'mocha-webdriver'; +import { $, gu, test } from 'test/nbrowser/gristUtil-nbrowser'; + +describe('UndoJumps.ntest', function() { + const cleanup = test.setupTestSuite(this); + + before(async function() { + await gu.supportOldTimeyTestCode(); + await gu.useFixtureDoc(cleanup, "WorldUndo.grist", true); + }); + + afterEach(function() { + return gu.checkForErrors(); + }); + + async function clickCellAndCheck(pos, text) { + let cell = gu.getCell(pos); + await cell.click(); + assert.equal(await cell.text(), text); + } + + async function getSectionAndCursor() { + let section = await $('.active_section .test-viewsection-title').wait().text(); + let cursorPos = await gu.getCursorPosition(); + let text = await gu.getActiveCell().text(); + return Object.assign({section, text}, cursorPos); + } + + function beforePos(pos) { return Object.assign({}, pos, {text: pos.text[0]}); } + function afterPos(pos) { return Object.assign({}, pos, {text: pos.text[1]}); } + + let positions = []; + + it("test setup", async function() { + // In this pseudo-testcase, we do a bunch of actions, whose undo will require jumping around. + // We store expected positions along the way, to make it easier to know what to expect. + this.timeout(Math.max(this.timeout(), 20000)); // Long-running test, unfortunately + + // In City view, record section, change a cell on screen (row 3, col 1). + await gu.actions.selectTabView('City'); + positions.push({section: 'CITY', rowNum: 3, col: 0, text: ['Aalborg', 'rec-update1']}); + await clickCellAndCheck({section: 'City', rowNum: 3, col: 0}, 'Aalborg'); + await gu.sendKeys('rec-update1', $.ENTER); + await gu.waitForServer(); + + // Then scroll and change another cell off screen, near the bottom (row 4070, col 3). + await gu.sendKeys([$.MOD, $.DOWN]); + positions.push({section: 'CITY', rowNum: 4070, col: 2, text: ['Çorum', 'upd-2']}); + await clickCellAndCheck({section: 'CITY', rowNum: 4070, col: 2}, 'Çorum'); + await gu.sendKeys('upd-2', $.ENTER); + await gu.waitForServer(); + + // In City view, detail section, change a cell too (row 20, col 'Name'). + await gu.actions.viewSection('CITY Card List').selectSection(); + await gu.sendKeys([$.MOD, $.UP]); + await gu.sendKeys([$.MOD, 'F'], 'bogotá', $.ESCAPE); + // discard notification + await $(".test-notifier-toast-close").wait(100).click(); + positions.push({section: 'CITY Card List', rowNum: 20, col: 'Name', + text: ['Santafé de Bogotá', 'det-update']}); + let cell = gu.getDetailCell('Name', 20); + await cell.click(); + assert.equal(await cell.text(), 'Santafé de Bogotá'); + await gu.sendKeys('det-update', $.ENTER); + await gu.waitForServer(); + + // Switch to different view (Country), scroll to bottom and add a record (row 240, col 2). + await gu.actions.selectTabView('Country'); + await gu.sendKeys([$.MOD, $.DOWN], $.RIGHT); + positions.push({section: 'COUNTRY', rowNum: 240, col: 1, text: ['', 'country-add']}); + await gu.sendKeys('country-add', $.ENTER); + await gu.waitForServer(); + + // Switch back to City view, and add a record by inserting before row 10. + await gu.actions.selectTabView('City'); + await gu.actions.viewSection('City').selectSection(); + await gu.sendKeys([$.MOD, $.UP]); + positions.push({section: 'CITY', rowNum: 10, col: 1, text: ['United Kingdom', '']}); + await gu.clickCell({section: 'City', rowNum: 10, col: 1}); + await gu.sendKeys([$.MOD, $.SHIFT, $.ENTER]); + await gu.waitForServer(); + + // Switch back to Country view, delete a record (row 6) + await gu.actions.selectTabView('Country'); + await gu.sendKeys([$.MOD, $.UP]); + positions.push({section: 'COUNTRY', rowNum: 5, col: 1, text: ['Albania', 'Andorra']}); + await clickCellAndCheck({section: 'Country', rowNum: 5, col: 1}, 'Albania'); + await gu.sendKeys([$.MOD, $.DELETE]); + await gu.confirm(true, true); // confirm and remember + await gu.waitForServer(); + + // Switch back to City view, place cursor onto (row 8, col 'District'), delete column. + await gu.actions.selectTabView('City'); + await gu.sendKeys([$.MOD, $.UP]); + positions.push({section: 'CITY', rowNum: 7, col: 2, text: ['Hakassia', '169200']}); + await clickCellAndCheck({section: 'City', rowNum: 7, col: 2}, 'Hakassia'); + await gu.sendKeys([$.ALT, '-']); + await gu.waitForServer(); + + // Switch to Country view, and add a column. + await gu.actions.selectTabView('Country'); + positions.push({section: 'COUNTRY', rowNum: 4, col: 2, text: ['North America', '']}); + await clickCellAndCheck({section: 'Country', rowNum: 4, col: 2}, 'North America'); + await gu.sendKeys([$.ALT, $.SHIFT, '=']); + await gu.waitForServer(); + await gu.sendKeys($.ENTER); + }); + + async function check_undos() { + // Initial position, at the end of the setup (on a newly-added column). + assert.deepEqual(await getSectionAndCursor(), + {section: 'COUNTRY', rowNum: 4, col: 2, text: ''}); + + // Move to a different place. + await gu.clickCell({section: 'CountryLanguage', rowNum: 1, col: 'Percentage'}); + + // Now call undo repeatedly, comparing positions recorded in the `positions` list. + for (let i = positions.length - 1; i >= 0; i--) { + await gu.undo(); + assert.deepEqual(await getSectionAndCursor(), beforePos(positions[i]), + `Undo position #${i} doesn't match`); + } + + // Just to make sure these checks actually ran, verify where we are. + assert.deepEqual(await getSectionAndCursor(), + {section: 'CITY', rowNum: 3, col: 0, text: 'Aalborg'}); + assert.equal(positions.length, 8); + } + + it("should jump to position of last action on undo", async function() { + // Undo each action, verifying cursor position each time. + await check_undos(); + }); + + it("should jump to position of last action on redo", async function() { + // Redo each action, verifying cursor position each time. + + // Move to a different view/place. + await gu.actions.selectTabView('Country'); + await gu.clickCell({section: 'Country', rowNum: 239, col: 'Name'}); + await gu.clickCell({section: 'CountryLanguage', rowNum: 1, col: 'Percentage'}); + + // Now call redo repeatedly, verifying recorded positions. + for (let i = 0; i < positions.length; i++) { + await gu.redo(); + assert.deepEqual(await getSectionAndCursor(), afterPos(positions[i]), + `Redo position #${i} doesn't match`); + } + + // To make sure checks ran, verify where we are. + assert.deepEqual(await getSectionAndCursor(), + {section: 'COUNTRY', rowNum: 4, col: 2, text: ''}); + assert.equal(positions.length, 8); + }); + + it("should jump again on second undo after redo", async function() { + // Undo again, it should work the same way. + await check_undos(); + }); +}); diff --git a/test/nbrowser/Validations.ntest.js b/test/nbrowser/Validations.ntest.js new file mode 100644 index 00000000..a2c0fb77 --- /dev/null +++ b/test/nbrowser/Validations.ntest.js @@ -0,0 +1,101 @@ +import { removePrefix } from 'app/common/gutil'; +import { assert, driver } from 'mocha-webdriver'; +import { $, gu, test } from 'test/nbrowser/gristUtil-nbrowser'; + +describe('Validations.ntest', function() { + const cleanup = test.setupTestSuite(this); + + before(async function() { + await gu.supportOldTimeyTestCode(); + await gu.useFixtureDoc(cleanup, "Hello.grist", true); + await driver.executeScript(`window.gristApp.enableFeature('validationsTool', true)`); + }); + + afterEach(function() { + return gu.checkForErrors(); + }); + + it("can add empty validation, which should not show indicators", async function() { + // Open the validations config pane (lives in the Document side pane). + await gu.openSidePane('validate'); + await $('$Validation_addRule').wait(assert.isDisplayed); + + // There should be no validation lines to begin with. + assert.lengthOf(await $("$Validation_rules > .kf_row").array(), 0); + + // Create one, and wait for something to appear. + await $("$Validation_addRule").click(); + await $("$Validation_rules > .validation").wait(); + + // Now there should be one validation rule. + assert.lengthOf(await $("$Validation_rules > .validation").array(), 1); + + // Make sure there are no "validation failure" badgets. + assert.lengthOf(await $(".gridview_row .validation_error_number").array(), 0); + + // Change rule to fail always, and ensure there is a validation failure for each cell. + var formula = $("$Validation_rules .validation_formula").eq(0).find(".ace_editor").wait().elem(); + await formula.click(); + await gu.sendKeys('False'); + await $("$Validation_rules .kf_button:contains(Apply)").click(); + await gu.waitForServer(); + assert.lengthOf(await $(".gridview_row .validation_error_number").array(), 4); + + // Empty out the rule, and see that it now passes. + await driver.withActions(a => a.doubleClick(formula)); + await gu.sendKeys($.DELETE); + await $("$Validation_rules .kf_button:contains(Apply)").click(); + await gu.waitForServer(); + assert.lengthOf(await $(".gridview_row .validation_error_number").array(), 0); + }); + + /** + * Helper to fetch information about a validation failure badge. Returns an object with .text + * being the badge's text (count of failures), and .title being the title attribute. + */ + async function getRowValidation(rowIndex) { + try { + const elem = $(".gridview_data_row_num").eq(rowIndex).find(".validation_error_number"); + const text = await elem.getText(); + const title = await elem.getAttribute('title'); + return { text: text, title: removePrefix(title, "Validation failed: ") }; + } catch (e) { + if (/NoSuchElement/.test(String(e))) { return null; } + throw e; + } + } + + it("should show correct failure counts and messages", async function() { + // Enter some data into first column. + await gu.enterGridValues(0, 1, [["foo", "BAR", "17", ""]]); + await gu.waitForServer(); + + // Change rule to something non-trivial. + await $("$Validation_rules .validation_formula").eq(0).find(".ace_editor").click(); + await gu.sendKeys("$B.lower() == $B"); + await $(".validation").eq(0).findOldTimey(".kf_button:contains(Apply)").click(); // 2nd row should fail. + + // Add a rule that raises an exception. + await $("$Validation_addRule").click(); + await gu.waitForServer(); + await $("$Validation_rules .validation_formula").eq(1).find(".ace_editor").click(); + await gu.sendKeys("int($B) > 0"); + await $(".validation").eq(1).findOldTimey(".kf_button:contains(Apply)").click(); // Rows 1,2,3 should fail. + await gu.waitForServer(); + + // Assert correct number of badges, and correct numbers in them. + await gu.waitForServer(2000); + assert.lengthOf(await $(".gridview_row .validation_error_number").array(), 3); + assert.deepEqual(await getRowValidation(0), { text: "1", title: "Rule 2" }); + assert.deepEqual(await getRowValidation(1), { text: "2", title: "Rule 1, Rule 2" }); + assert.deepEqual(await getRowValidation(2), null); + assert.deepEqual(await getRowValidation(3), { text: "1", title: "Rule 2" }); + + // Now change some data and ensure badges and titles changed appropriately. + await gu.enterGridValues(0, 1, [["FOO", "100", "-17"]]); + assert.deepEqual(await getRowValidation(0), { text: "2", title: "Rule 1, Rule 2" }); + assert.deepEqual(await getRowValidation(1), null); + assert.deepEqual(await getRowValidation(2), { text: "1", title: "Rule 2" }); + assert.deepEqual(await getRowValidation(3), { text: "1", title: "Rule 2" }); + }); +}); diff --git a/test/nbrowser/ViewConfigTab.ntest.js b/test/nbrowser/ViewConfigTab.ntest.js new file mode 100644 index 00000000..2f44a568 --- /dev/null +++ b/test/nbrowser/ViewConfigTab.ntest.js @@ -0,0 +1,71 @@ +import { assert } from 'mocha-webdriver'; +import { $, gu, test } from 'test/nbrowser/gristUtil-nbrowser'; + +describe('ViewConfigTab.ntest', function() { + test.setupTestSuite(this); + + before(async function() { + await gu.supportOldTimeyTestCode(); + + await gu.actions.createNewDoc(); + }); + + afterEach(function() { + return gu.checkForErrors(); + }); + + it('should set up a new Untitled document with one table', async function() { + // Add another table to the document: click dropdown, select "New Table". + assert.deepEqual(await $(`.test-docpage-label`).array().text(), ['Table1']); + await gu.actions.addNewTable(); + assert.deepEqual(await $(`.test-docpage-label`).array().text(), ['Table1', 'Table2']); + }); + + it("should allow opening and closing view config pane", async function() { + var viewNameInput = $('.test-right-widget-title'); + + // Open the tab, and check it becomes visible. + await gu.openSidePane('view'); + await assert.isDisplayed(viewNameInput.wait(assert.isDisplayed), true); + + // Close the tab again. + await gu.toggleSidePanel('right', 'toggle'); + await assert.isPresent(viewNameInput, false); + }); + + it('should keep view config pane open across views, with correct view name', async function() { + await gu.openSidePane('view'); + + var viewNameInput = $('.test-right-widget-title'); + await assert.isDisplayed(viewNameInput.wait(assert.isDisplayed), true); + assert.equal(await viewNameInput.val(), 'TABLE2'); + + // Switch to another view, and make sure the view config side pane is still visible. + await gu.actions.selectTabView('Table1'); + await assert.isDisplayed(viewNameInput); + assert.equal(await viewNameInput.val(), 'TABLE1'); + + // Switch to a third view. + await gu.actions.selectTabView('Table2'); + await assert.isDisplayed(viewNameInput); + assert.equal(await viewNameInput.val(), 'TABLE2'); + }); + + it('should allow renaming the view from view config pane', async function() { + await gu.actions.selectTabView('Table2'); + + // Select the view name text box, select the text and replace with "Hello". + var viewNameInput = $('.test-right-widget-title'); + await gu.renameTable('Table2', 'Hello'); // I think renaming happens differently now? + + // Make sure there is now a view in Table named "Hello" + let tabs = await $(`.test-docpage-label`).array().text(); + assert.deepEqual(tabs, [ 'Table1', 'Hello' ]); + + // Switch to another view, and back to Hello, and see that the view name textbox is correct. + await gu.actions.selectTabView('Table1'); + assert.equal(await viewNameInput.val(), 'TABLE1'); + await gu.actions.selectTabView('Hello'); + assert.equal(await viewNameInput.val(), 'HELLO'); + }); +}); diff --git a/test/nbrowser/Views.ntest.js b/test/nbrowser/Views.ntest.js new file mode 100644 index 00000000..d59c31c0 --- /dev/null +++ b/test/nbrowser/Views.ntest.js @@ -0,0 +1,143 @@ +import { assert } from 'mocha-webdriver'; +import { $, gu, test } from 'test/nbrowser/gristUtil-nbrowser'; + +describe('Views.ntest', function() { + test.setupTestSuite(this); + + before(async function() { + await gu.supportOldTimeyTestCode(); + await gu.actions.createNewDoc(); + }); + + afterEach(function() { + return gu.checkForErrors(); + }); + + it('should allow adding and removing viewsections', async function() { + // Create two new viewsections + await gu.actions.addNewSection('Table1', 'Table'); + var recordSection = $('.test-gristdoc .view_leaf').eq(0); + var gridSection = $('.test-gristdoc .view_leaf').eq(1); + + // Check that the newest viewsection has focus + await assert.hasClass(gridSection, 'active_section'); + + await gu.actions.addNewSection('Table1', 'Card'); + var cardSection = $('.test-gristdoc .view_leaf').eq(2); + assert.lengthOf(await $('.test-gristdoc .view_leaf').array(), 3); + + // Check that the newest viewsection has focus + await assert.hasClass(cardSection, 'active_section'); + + // Click the second viewsection and check that it has focus + await gridSection.click(); + await assert.hasClass(gridSection, 'active_section'); + + // Check that viewsection titles are correct and editable + var recordTitle = recordSection.find('.test-viewsection-title'); + assert.equal(await recordTitle.text(), 'TABLE1'); + await recordTitle.click(); + await gu.renameActiveSection('foo'); + assert.equal(await recordTitle.text(), 'foo'); + + // Delete the first viewsection and check that it`s gone + await gu.actions.viewSection('foo').selectMenuOption('viewLayout', 'Delete widget'); + await gu.waitForServer(); + + assert.lengthOf(await $('.test-gristdoc .view_leaf').array(), 2); + }); + + it('should allow creating a view section for a new table', async function() { + assert.lengthOf(await $('.test-gristdoc .view_leaf').array(), 2); + await gu.actions.addNewSection('New', 'Card List'); + var newTitle = $('.test-gristdoc .test-viewsection-title').eq(2).wait(); + assert.equal(await newTitle.text(), 'TABLE2 Card List'); + }); + + it('should not create two views when creating a new view for a new table', async function() { + // This is probably test for a bug. Currently adding new tables to an existing view + // doesn't produce new views. + assert.deepEqual(await $(`.test-docpage-label`).array().text(), ['Table1']); + await gu.actions.addNewView('New', 'Table'); + assert.deepEqual(await $(`.test-docpage-label`).array().text(), ['Table1', 'Table3']); + }); + + it('should switch to a valid default view when the active view is deleted', async function() { + // This confirms a bug fix where the default view should change when the current + // default view is deleted + await gu.actions.selectTabView('Table1'); + assert.equal(await gu.actions.getActiveTab().text(), 'Table1'); + assert.deepEqual(await $(`.test-docpage-label`).array().text(), ['Table1', 'Table3']); + await gu.actions.tableView('Table1').selectOption('Remove'); + await $(".test-removepage-option-page").click(); + await $(".test-modal-confirm").click(); + await gu.waitForServer(); + assert.equal(await gu.actions.getActiveTab().text(), 'Table3'); + assert.deepEqual(await $(`.test-docpage-label`).array().text(), ['Table3']); + }); + + it('should allow adding and removing summary view sections', async function() { + await gu.removeTable("Table1"); + await gu.actions.addNewTable(); + assert.equal(await gu.actions.getActiveTab().text(), 'Table1'); + + await gu.enterGridValues(0, 0, [ + ['a', 'a', 'b'], + ['c', 'd', 'd'], + ['1', '2', '3'] + ]); + + await gu.actions.addNewSummarySection('Table1', ['A'], 'Table', 'Section Foo'); + assert.deepEqual(await gu.getGridValues({ + section: 'Section Foo', + rowNums: [1, 2], + cols: [0, 1, 2] + }), ['a', '2', '3', 'b', '1', '3']); + + await gu.actions.viewSection('Section Foo').selectMenuOption('viewLayout', 'Delete widget'); + await gu.waitForServer(); + + await gu.actions.addNewSummarySection('Table1', ['B'], 'Table', 'Section Foo'); + assert.deepEqual(await gu.getGridValues({ + section: 'Section Foo', + rowNums: [1, 2], + cols: [0, 1, 2] + }), ['c', '1', '1', 'd', '2', '5']); + }); + + it('should switch to a valid default section when the active section is deleted', async function() { + // This confirms a bug fix where the section should change when the active section is + // deleted, either directly or via the section's table being deleted. + // Reference: https://phab.getgrist.com/T327 + await gu.actions.addNewSection('New', 'Table'); + await gu.waitForServer(); + await gu.getSection('TABLE4').click(); + // Delete the section + await gu.actions.viewSection('TABLE4').selectMenuOption('viewLayout', 'Delete widget'); + await gu.waitForServer(); + // Assert that the default section (Table1 record) is now active. + assert.equal(await $('.active_section > .viewsection_title').text(), 'TABLE1'); + // Assert that focus is returned to the deleted section on undo. + await gu.undo(); + assert.equal(await $('.active_section > .viewsection_title').text(), 'TABLE4'); + // Add a sorted column to the new table. The reported bug shows a symptom of the problem is + // errors thrown when a sorted column exists in the deleted table. + await gu.clickCellRC(0, 0); + await gu.sendKeys('b', $.ENTER); + await gu.waitForServer(); + await gu.sendKeys('a', $.ENTER); + await gu.waitForServer(); + await gu.sendKeys('c', $.ENTER); + await gu.waitForServer(); + await gu.openColumnMenu('A'); + await $(`.grist-floating-menu .test-sort-asc`).click(); + // Delete the table. + await gu.removeTable("Table4"); + await gu.actions.selectTabView('Table1'); + // Assert that the default section (Table1 record) is now active. + assert.equal(await $('.active_section > .viewsection_title').text(), 'TABLE1'); + // Again, assert that focus is returned to the deleted section on undo. + await gu.undo(); + assert.equal(await $('.active_section > .viewsection_title').text(), 'TABLE4'); + }); +}); diff --git a/test/nbrowser/WebhookPage.ts b/test/nbrowser/WebhookPage.ts index c438d49d..a236674b 100644 --- a/test/nbrowser/WebhookPage.ts +++ b/test/nbrowser/WebhookPage.ts @@ -8,7 +8,7 @@ import { server } from 'test/nbrowser/testUtils'; //import { Deps as TriggersDeps } from 'app/server/lib/Triggers'; describe('WebhookPage', function () { - this.timeout(30000); + this.timeout(60000); const cleanup = setupTestSuite(); let session: gu.Session; diff --git a/test/nbrowser/gristUtil-nbrowser.js b/test/nbrowser/gristUtil-nbrowser.js new file mode 100644 index 00000000..4187c25d --- /dev/null +++ b/test/nbrowser/gristUtil-nbrowser.js @@ -0,0 +1,679 @@ +import * as _ from 'lodash'; +import { assert, driver, Key, stackWrapFunc, WebElement, + WebElementPromise } from 'mocha-webdriver'; +import { driverCompanion, findOldTimey, waitImpl, + webdriverjqWrapper } from 'test/nbrowser/webdriverjq-nbrowser'; +import * as guBase from 'test/nbrowser/gristUtils'; +import { setupTestSuite } from 'test/nbrowser/testUtils'; +import { server } from 'test/nbrowser/testServer'; + +// Simulate the old "$" object. + +const _webdriverjqFactory = webdriverjqWrapper(driver); +function $(key) { + if (typeof key !== 'string') { + return 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); + }; + $.wait = (timeoutMs, conditionFunc) => { + return waitImpl(timeoutMs, conditionFunc); + } + for (const key of Object.keys(Key)) { + $[key] = Key[key]; + } + // We need to tweak driver object a bit too (really?) + driverCompanion.$ = $; + // driver.testHost = server.getHost(); + // driver.waitImpl = waitImpl; +} + +// Adapt common old setup. +const test = { + setupTestSuite(self, ...args) { + self.timeout(20000); + return setupTestSuite(...args); + }, +}; + +// Add some methods to the grist utils that are used by old tests. +// This could be cleaned up further, but translating to newer ways +// of doing things or, if the method is really useful, adding the +// method to the new grist utils. + +const waitForServer = guBase.waitForServer; +let patchesApplied = false; +let session; + +const gu = { + ...guBase, + + // Apply all needed patches, async initialization, and log in. + async supportOldTimeyTestCode() { + if (!patchesApplied) { + applyPatchesToWebElements(); + applyPatchesToAssert(); + applyPatchesToJquerylikeObject($); + } + patchesApplied = true; + // Login as someone so old code doesn't have to be upgraded to do it. + session = await gu.session().user('userz'); + const dbManager = await server.getDatabase(); + const profile = {email: session.email, name: session.name}; + await dbManager.getUserByLogin(session.email, {profile}); + await gu.setApiKey(session.name); + await session.login(); + }, + + // getCell with old-timey arguments. + getCellRC(r, c) { + return gu.getCell(c, r + 1); + }, + + // clickCell with old-timey arguments. + async clickCellRC(r, c) { + const cell = gu.getCell(c, r + 1); + await cell.click(); + return cell; + }, + + // sendKeys variant that accepts arrays in place of Key.chord. + sendKeys(...args) { + return guBase.sendKeys(...args.map( + a => Array.isArray(a) ? Key.chord(...a) : a + )); + }, + + /** + * When doing type conversion in the side pane, this clicks the 'Apply' button and waits for the + * conversion to complete. + */ + async applyTypeConversion() { + await $('.test-type-transform-apply').wait().scrollIntoView({ + block: 'nearest', + inline: 'nearest', + }).click(); + await $('.test-type-transform-apply').waitDrop(assert.isPresent, false); + return gu.waitForServer(); + }, + + async clickColumnMenuItem(colName, itemText, optRightClick) { + await gu.openColumnMenu(colName); + return gu.actions.selectFloatingOption(itemText); + }, + + getOpenEditingLabel(parentElem) { + return driver.find('.test-column-title-label'); + }, + + enterGridValues(startRowIndex, startColIndex, dataMatrix) { + const transpose = dataMatrix[0].map( + (_, colIndex) => dataMatrix.map(row => row[colIndex]) + ); + return gu.enterGridRows({col: startColIndex, + rowNum: startRowIndex + 1}, transpose); + }, + + async openSidePane(tabName) { + if (['log', 'validate', 'repl', 'code'].includes(tabName)) { + await guBase.toggleSidePanel('right', 'open'); + return $(`.test-tools-${tabName}`).wait().click(); + } else if (tabName === 'field') { + await guBase.toggleSidePanel('right', 'open'); + return $('.test-right-tab-field').click(); + } else if (tabName === 'view') { + await guBase.toggleSidePanel('right', 'open'); + return $('.test-right-tab-pagewidget').wait().click(); + } + }, + + getGridValues(...options) { + return gu.getVisibleGridCells(...options); + }, + + /** + * Returns the text in the row header of the last row in a Grid section, scrolling to the + * bottom, but not moving the cursor. + * @param {String} options.section: Optional section name to use instead of the active section. + */ + async getGridLastRowText(options) { + if (options?.section) { + await gu.actions.viewSection(options.section).selectSection(); + } + return String(await gu.getGridRowCount()); + }, + + getAddRowNumber() { + return gu.getGridRowCount(); + }, + + async getGridLabels(sectionName) { + return gu.getSection(sectionName).findAll('[data-test-id="GridView_columnLabel"] .kf_elabel_text', + label => label.getText()); + }, + + /** + * Given a cell in grist GridView, returns whether it contains the cursor. You may use it as + * hasCursor(getCell(...)) or getCell(...).waitFor(hasCursor, optTimeout). + */ + hasCursor(cellElem) { + return cellElem.find('.selected_cursor').isDisplayed(); + }, + + async useFixtureDoc(cleanup, fname, flag) { + return session.tempDoc(cleanup, fname); + }, + + async copyDoc(docId, flag) { + const result = await guBase.copyDoc(session.name, 'docs', 'Home', docId); + await session.loadDoc(`/doc/${result.id}`); + return result; + }, + + async clickCell(rowIndexOrPosOrCell, colIndex) { + if (typeof rowIndexOrPosOrCell === 'object' && 'driver_' in rowIndexOrPosOrCell) { + return rowIndexOrPosOrCell.click(); + } + // Best just to force a rewrite of clickCell, e.g. to clickCellRC, + // since newer gristUtils interprets arguments entirely differently. + if (typeof rowIndexOrPosOrCell === 'number') { + throw new Error("ambiguous row/col"); + } + const cell = guBase.getCell(rowIndexOrPosOrCell, colIndex); + await cell.click(); + }, + + /** + * Sets the visibleCol of the currently selected field to value. + */ + async setVisibleCol(value) { + await gu.openSidePane('field'); + await $('.test-fbuilder-ref-col-select').click(); + await $(`.test-select-menu .test-select-row:contains(${value})`).wait().click(); + return waitForServer(); + }, + + /** + * Asserts the type of the currently selected field. + */ + async assertType(value) { + await gu.openSidePane('field'); + assert.equal(await $('.test-fbuilder-type-select .test-select-row').getText(), value); + }, + + closeSidePane() { + return gu.toggleSidePanel('right', 'close'); + }, + + clickVisibleDetailCells(column, rowNums) { + return gu.getDetailCell(column, rowNums[0]).click(); + }, + + async clickRowMenuItem(rowNum, item) { + await (await gu.openRowMenu(rowNum)).findContent('li', item).click(); + }, + + /** + * Selects rows starting from rowStart and ending at rowEnd (1-based) by clicking and dragging or + * shift clicking (Defaults to dragging) + * @param {int} rowStart: 1-based row number + * @param {int} rowEnd: 1-based row number. + * @param {String} optMethod: if 'shift' then shift clicking is used to select rows otherwise it + * defaults to drag to select. + */ + async selectRows(rowStart, rowEnd, optMethod) { + let start = await driver.findContent('.active_section .gridview_data_row_num', gu.exactMatch(rowStart.toString())); + let end = await driver.findContent('.active_section .gridview_data_row_num', gu.exactMatch(rowEnd.toString())); + if (optMethod === 'shift') { + await driver.withActions(a => a.click(start).keyDown($.SHIFT).click(end).keyUp($.SHIFT)); + } else { + await driver.withActions(a => a.move({origin: start}).press().move({origin: end}).release()); + } + }, + + async _fieldSettingsClickOption(isCommonToSeparate, optionSubstring) { + assert.include(await $('.fieldbuilder_settings_button').text(), isCommonToSeparate ? 'Common' : 'Separate'); + await $('.fieldbuilder_settings_button').click(); + await gu.actions.selectFloatingOption(optionSubstring); + await waitForServer(); + assert.include(await $('.fieldbuilder_settings_button').text(), isCommonToSeparate ? 'Separate' : 'Common'); + }, + fieldSettingsUseSeparate: () => gu._fieldSettingsClickOption(true, 'Use separate'), + fieldSettingsSaveAsCommon: () => gu._fieldSettingsClickOption(false, 'Save as common'), + fieldSettingsRevertToCommon: () => gu._fieldSettingsClickOption(false, 'Revert to common'), + + /** + * Changes date format for date and datetime editor or returns current format + * @param {string} value Date format + */ + async dateFormat(value) { + if (!value) { + return $('$Widget_dateFormat .test-select-row').text(); + } + await $('$Widget_dateFormat').wait().click(); + await $(`.test-select-menu .test-select-row:contains(${value})`).wait().click(); + }, + + /** + * Changes time format for datetime editor or returns current format + * @param {string} value Time format + */ + async timeFormat(value) { + if (!value) { + return $('$Widget_timeFormat .test-select-row').getText(); + } + await $('$Widget_timeFormat').wait().click(); + await $(`.test-select-menu .test-select-row:contains(${value})`).wait().click(); + }, + + async getDetailValues(...options) { + return gu.getVisibleDetailCells(...options); + }, + + /** + * Selects all cells in a GridView between and including startCell and endCell + * @param {Array} startCell: + * startCell[0]: 1-based row index. + * startCell[1]: 0-based column index. + * @param {Array} endCell: + * endCell[0]: 1-based row index. + * 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)); + }, + + /** + * Returns text of the cells for the given rows and columns of a viewSection. + * @param {String} option.section: Optional section name instead of active. + * @param {Array} option.rowNums: Array of row numbers (1-based) + * @param {Array} option.cols: Array of column indices (0-based) or labels. + * @param [Number: Function] option.cellFunc: a function that returns cells given an array of + * columns, rows and optionally a viewsection (defaults to the currently active section) + * @param [Number: Function] option.valueFunc: Optional function, or an object mapping column + * index or label (as in options.cols) to function, with the function mapping a cell to + * its value (by default, cell => cell.text()). + * @returns {Promise} Returns array of values for each requested cell, as all values from + * the first row, followed by values from the second, etc. + */ + async getSectionValues(options) { + var opts = { section: options.section }; + var defaultValueFunc = (cell => cell.text()); + var valueFunc; + if (options.valueFunc && !_.isFunction(options.valueFunc)) { + valueFunc = (col => options.valueFunc[col] || defaultValueFunc); + } else { + valueFunc = _.constant(options.valueFunc || defaultValueFunc); + } + const colValues = []; + for (const col of options.cols) { + const colValue = await valueFunc(col)(options.cellFunc(col, options.rowNums, opts).array()); + colValues.push(colValue); + } + return _.flatten(_.zip.apply(_, colValues), true); + }, + + /** + * Asserts the widget of the currently selected field. + */ + async assertWidget(value) { + await gu.openSidePane('field'); + assert.equal(await $('.test-fbuilder-widget-select .test-select-row').getText(), value); + }, + + /** + * Sets the widget of the currently selected field to value. + */ + async setWidget(value) { + await gu.openSidePane('field'); + const selector = $('.test-fbuilder-widget-select'); + const btnChildren = await selector.elem().findAll('.test-select-button'); + if (btnChildren.length > 0) { + // This is a button select. + await selector.findOldTimey(`.test-select-button:contains(${value})`).click(); + } else { + // This is a dropdown select. + await selector.click(); + await $(`.test-select-menu .test-select-row:contains(${value})`).click(); + } + await gu.waitForServer(); + }, + + /** + * Adds a new record to the grid. Takes an array of values that matches column positions. + */ + async addRecord(values) { + await gu.sendKeys([$.MOD, $.UP]); + await gu.sendKeys([$.MOD, $.DOWN]); + await gu.sendKeys([$.LEFT]); + await gu.sendKeys([$.LEFT]); + await gu.sendKeys([$.LEFT]); + await gu.sendKeys([$.LEFT]); + await gu.sendKeys([$.LEFT]); + await driver.sleep(1000); + // For each value, type it, followed by Tab. + for (const [i, value] of values.entries()) { + await gu.waitAppFocus(true); + await gu.sendKeys(value, $.TAB); + await gu.waitForServer(); + if (i === 0) { + // The very first value triggers add-record, but the creation of the new row isn't + // immediate, so give it a moment. + await driver.sleep(250); + } + } + // Return a promise that can be awaited; it will wait for all the previously queued ones. + return driver.sleep(0); + }, + + actions: { + createNewDoc: async (optDocName) => { + await gu.simulateLogin("Chimpy", "chimpy@getgrist.com", "nasa"); + const docId = await gu.createNewDoc('chimpy', 'nasa', 'Horizon', optDocName || 'Untitled'); + await gu.loadDoc(`/o/nasa/doc/${docId}`); + }, + getDocTitle: () => { + return $('.test-bc-doc').val(); + }, + getActiveTab: () => { + return $('.test-treeview-itemHeader.selected .test-docpage-label').wait(); + }, + getTabs: () => { + return $('.test-docpage-label'); + }, + renameDoc: (newName) => { + $('.test-bc-doc').click(); + $.driver.sendKeys(newName, $.ENTER); + return $.wait(1000, () => $.driver.getTitle().startsWith(newName + ' - ')); + }, + selectTabView: async (viewTitle) => { + const isOpen = await gu.isSidePanelOpen('left'); + if (!isOpen) { + await gu.toggleSidePanel('left', 'open'); + } + await gu.openPage(viewTitle); + if (!isOpen) { + await gu.toggleSidePanel('left', 'close'); + } + }, + addNewTable: async () => { + await $('.test-dp-add-new').wait().click(); + await $('.test-dp-empty-table').click(); + // if we selected a new table, there will be a popup for a name + const prompts = await $(".test-modal-prompt"); + const prompt = prompts[0]; + if (prompt) { + await await $(".test-modal-confirm").click(); + } + return gu.waitForServer(); + }, + addNewSection: (tableId, sectionType) => { + return gu.addNewSection(sectionType, tableId); + }, + addNewSummarySection: async (tableId, groupByArr, sectionType, sectionName) => { + await gu.addNewSection(sectionType, tableId, {summarize: groupByArr}); + await gu.waitForServer(); + await gu.renameActiveSection(sectionName); + await gu.waitForServer(); + }, + addNewView: (tableId, sectionType) => { + return gu.addNewPage(sectionType, tableId); + }, + selectFloatingOption: async (optionName) => { + // Sometimes the element is there but "not interactable". Work around that. + await gu.waitToPass(async () => { + await $(`.grist-floating-menu li:contains(${optionName})`).click(); + }); + }, + /** + * Actions related to view section. To use, pass in the section name. + * @param {string} sectionName - Title of the view section + * @return Object} Collection of methods for the view section. + * + * @example + * gu.actions.viewSection('Table1 record').selectMenuOption('Insert section'); + */ + viewSection: (sectionName) => { + let section = gu.getSection(sectionName); + return { + /** + * Clicks inside to make the current section active. + */ + selectSection: function () { + return gu.selectSectionByTitle(sectionName); + }, + /** + * Opens the view section drop-down menu. + * @param {string} which - Which menu to open, coud be: 'sortAndFilter' or 'viewLayout' + */ + openMenu: async function (which) { + await driver.withActions(a => a.move({origin: section.find('.viewsection_title')})); // to display menu buttons on hover + const item = section.find(`.test-section-menu-${which}`); + await gu.waitToPass(() => item.click()); + }, + /** + * Opens the section drop-down menu and select option matching param. + * @param {string} which - Which menu to open, coud be: 'sortAndFilter' or 'viewLayout' + * @param {string} optionName + */ + selectMenuOption: function (which, optionName) { + this.openMenu(which); + return gu.actions.selectFloatingOption(optionName); + } + }; + }, + tableView: (tableName, viewName) => { + return { + select: () => { + return gu.getPageItem(tableName).click(); + }, + selectOption: async optionName => { + await gu.openPageMenu(tableName); + return gu.actions.selectFloatingOption(optionName); + } + }; + } + } +}; + +/** + * This monkey-patches the WebElement class to make it look enough like + * jquery that a lot of old test code can be used without modification. + */ +function applyPatchesToWebElements() { + + WebElement.prototype.wait = function(fn, ...args) { + if (fn) { + return gu.waitToPass(async () => { + return fn.apply(null, [this, ...args]); + }).then(() => true); + } else { + return new WebElementPromise( + driver, + gu.waitToPass(async () => { + if (!(await this.isPresent())) { + throw new Error('not present'); + } + }).then(() => this)); + } + } + + WebElement.prototype.selected = function(val) { + return driver.executeScript((elem, val) => { + elem.selected = val; + }, this, val); + } + + WebElement.prototype.attr = function(key, val) { + if (val !== undefined) { + return driver.executeScript((elem, key, val) => { + elem.setAttribute(key, val); + }, this, key, val); + } + return this.getAttribute(key); + } + + WebElement.prototype.classList = async function() { + return (await this.getAttribute('className')).split(' '); + } + + // Lists of WebElements work differently - if we did a find() we + // already have just the first match. + WebElement.prototype.first = function() { + return this; + } + + WebElement.prototype.text = function() { + return this.getText(); + } + + WebElement.prototype.val = function(newVal) { + if (newVal === undefined) { + return this.getAttribute('value'); + } + return gu.setValue(this, newVal); + } + + WebElement.prototype.css = function(key, val) { + if (val === undefined) { + return this.getCssValue(key); + } + return new WebElementPromise( + driver, + driver.executeScript(elem => { + elem.style[key] = val; + }, this) + ); + } + + WebElement.prototype.is = function(selector) { + return this.matches(selector); + } + + WebElement.prototype.hasClass = async function(className) { + return (await this.classList()).includes(className); + } + + WebElement.prototype.scrollIntoView = function(opts) { + opts = opts || {behavior: 'auto'}; + return new WebElementPromise( + driver, + driver.executeScript((elem, opts) => elem.scrollIntoView(opts), + this, opts).then(() => this)); + } + + WebElement.prototype.parent = function() { + return new WebElementPromise( + driver, + driver.executeScript(elem => { + return elem.parentNode.closest('*'); + }, this) + ); + } + + WebElement.prototype.closest = function(key) { + return this.findClosest(key); + } + + WebElement.prototype.children = async function(mapper) { + // Collect children. + let result = await driver.executeScript(elem => { + return [...elem.children].map(c => c.closest('*')); + }, this); + // Fix up type. + result = result.map(v => new WebElementPromise( + driver, + Promise.resolve(v), + )); + // Apply mapper if available. + if (mapper) { + result = result.map(mapper); + } + // Result is a single promise. + return Promise.all(result); + } + + WebElement.prototype.trimmedText = async function() { + const text = await this.getText(); + return text.trim(); + } + + // A version of find() that supports some old timey syntax. + WebElement.prototype.findOldTimey = function(key) { + return findOldTimey(this, key); + } + + WebElement.prototype.findAllOldTimey = function(key, mapper) { + return findOldTimey(this, key, true, mapper); + } +} + +/** + * This monkey-patches assert to add some methods that are very + * commonly used. + */ +function applyPatchesToAssert() { + + assert.hasClass = stackWrapFunc(async function(elem, className, present) { + if (present === undefined) { + present = true; + } + const c = await elem.getAttribute('class'); + if (present) { + await assert.include(c.split(' '), className); + } else { + await assert.notInclude(c.split(' '), className); + } + }); + + assert.isPresent = stackWrapFunc(async function(elem, present) { + if (present === undefined) { + present = true; + } + let current = false; + try { + current = await elem.isPresent(); + } catch (e) { + // $ object may fail if elem is non-existent. + } + await assert.equal(current, present); + return true; + }); + + assert.isDisplayed = stackWrapFunc(async function(elem, displayed) { + if (displayed === undefined) { + displayed = true; + } + await assert.equal(await elem.isDisplayed(), displayed); + return true; + }); +} + +exports.$ = $; +exports.gu = gu; +exports.server = server; +exports.test = test; diff --git a/test/nbrowser/gristUtils.ts b/test/nbrowser/gristUtils.ts index d8c8423b..c78a2475 100644 --- a/test/nbrowser/gristUtils.ts +++ b/test/nbrowser/gristUtils.ts @@ -1,5 +1,5 @@ /** - * Replicates functionality of test/browser/gristUtils.ts for new-style tests. + * Replicates functionality of test/nbrowser/gristUtils.ts for new-style tests. * * The helpers are themselves tested in TestGristUtils.ts. */ @@ -33,15 +33,20 @@ import type { AssertionError } from 'assert'; namespace gristUtils { // Allow overriding the global 'driver' to use in gristUtil. -let driver: WebDriver; +let _driver: WebDriver|undefined; +const driver: WebDriver = new Proxy({} as any, { + get(_, prop) { + if (!_driver) { + return (driverOrig as any)[prop]; + } + return (_driver as any)[prop]; // eslint-disable-line @typescript-eslint/no-unnecessary-type-assertion + } +}); export function currentDriver() { return driver; } // Substitute a custom driver to use with gristUtils functions. Omit argument to restore to default. -export function setDriver(customDriver: WebDriver = driverOrig) { driver = customDriver; } - -// Set the 'driver' to use here in before() callback, because driverOrig isn't set until then. -before(() => setDriver()); +export function setDriver(customDriver?: WebDriver) { _driver = customDriver; } const homeUtil = new HomeUtil(testUtils.fixturesRoot, server); diff --git a/test/nbrowser/homeUtil.ts b/test/nbrowser/homeUtil.ts index cb7e1653..82da0369 100644 --- a/test/nbrowser/homeUtil.ts +++ b/test/nbrowser/homeUtil.ts @@ -15,8 +15,9 @@ import {DocWorkerAPI, UserAPI, UserAPIImpl} from 'app/common/UserAPI'; import {HomeDBManager} from 'app/gen-server/lib/HomeDBManager'; import log from 'app/server/lib/log'; import {TestingHooksClient} from 'app/server/lib/TestingHooks'; +import EventEmitter = require('events'); -export interface Server { +export interface Server extends EventEmitter { driver: WebDriver; getTestingHooks(): Promise; getHost(): string; @@ -53,7 +54,11 @@ export class HomeUtil { // of the home api available while making browser tests. private _apiKey = new Map(); - constructor(public fixturesRoot: string, public server: Server) {} + constructor(public fixturesRoot: string, public server: Server) { + server.on('stop', () => { + this._apiKey.clear(); + }); + } public get driver(): WebDriver { return this.server.driver; } @@ -134,7 +139,8 @@ export class HomeUtil { // Take this opportunity to cache access info. if (!this._apiKey.has(email)) { await this.driver.get(this.server.getUrl(org || 'docs', '')); - this._apiKey.set(email, await this._getApiKey()); + const apiKey = await this._getApiKey(); + this._apiKey.set(email, apiKey); } } } diff --git a/test/nbrowser/testServer.ts b/test/nbrowser/testServer.ts index 54b91da1..8bcce502 100644 --- a/test/nbrowser/testServer.ts +++ b/test/nbrowser/testServer.ts @@ -18,6 +18,7 @@ import {makeGristConfig} from 'app/server/lib/sendAppPage'; import {exitPromise} from 'app/server/lib/serverUtils'; import {connectTestingHooks, TestingHooksClient} from 'app/server/lib/TestingHooks'; import {ChildProcess, execFileSync, spawn} from 'child_process'; +import EventEmitter from 'events'; import * as fse from 'fs-extra'; import {driver, IMochaServer, WebDriver} from 'mocha-webdriver'; import fetch from 'node-fetch'; @@ -27,7 +28,7 @@ import {removeConnection} from 'test/gen-server/seed'; import {HomeUtil} from 'test/nbrowser/homeUtil'; import {getDatabase} from 'test/testUtils'; -export class TestServerMerged implements IMochaServer { +export class TestServerMerged extends EventEmitter implements IMochaServer { public testDir: string; public testDocDir: string; public testingHooks: TestingHooksClient; @@ -42,10 +43,12 @@ export class TestServerMerged implements IMochaServer { private _exitPromise: Promise; private _starts: number = 0; private _dbManager?: HomeDBManager; - private _driver: WebDriver; + private _driver?: WebDriver; // The name is used to name the directory for server logs and data. - constructor(private _name: string) {} + constructor(private _name: string) { + super(); + } public async start() { await this.restart(true); @@ -66,9 +69,10 @@ export class TestServerMerged implements IMochaServer { if (process.env.TESTDIR) { this.testDir = process.env.TESTDIR; } else { - // Create a testDir of the form grist_test_{USER}_{SERVER_NAME}, removing any previous one. + const workerId = process.env.MOCHA_WORKER_ID || '0'; + // Create a testDir of the form grist_test_{USER}_{SERVER_NAME}_{WORKER_ID}, removing any previous one. const username = process.env.USER || "nobody"; - this.testDir = path.join(tmpdir(), `grist_test_${username}_${this._name}`); + this.testDir = path.join(tmpdir(), `grist_test_${username}_${this._name}_${workerId}`); await fse.remove(this.testDir); } } @@ -95,6 +99,9 @@ export class TestServerMerged implements IMochaServer { // logging. Server code uses a global logger, so it's hard to separate out (especially so if // we ever run different servers for different tests). const serverLog = process.env.VERBOSE ? 'inherit' : nodeLogFd; + const workerId = parseInt(process.env.MOCHA_WORKER_ID || '0', 10); + const corePort = String(8295 + workerId * 2); + const untrustedPort = String(8295 + workerId * 2 + 1); const env: Record = { TYPEORM_DATABASE: this._getDatabaseFile(), GRIST_DATA_DIR: this.testDocDir, @@ -107,23 +114,25 @@ export class TestServerMerged implements IMochaServer { GRIST_MAX_UPLOAD_ATTACHMENT_MB: '2', // The following line only matters for testing with non-localhost URLs, which some tests do. GRIST_SERVE_SAME_ORIGIN: 'true', - APP_UNTRUSTED_URL : "http://localhost:18096", // Run with HOME_PORT, STATIC_PORT, DOC_PORT, DOC_WORKER_COUNT in the environment to override. ...(useSinglePort ? { APP_HOME_URL: this.getHost(), GRIST_SINGLE_PORT: 'true', } : (isCore ? { - HOME_PORT: '8095', - STATIC_PORT: '8095', - DOC_PORT: '8095', + HOME_PORT: corePort, + STATIC_PORT: corePort, + DOC_PORT: corePort, DOC_WORKER_COUNT: '1', - PORT: '8095', + PORT: corePort, + APP_UNTRUSTED_URL: `http://localhost:${untrustedPort}`, + GRIST_SERVE_PLUGINS_PORT: untrustedPort, } : { HOME_PORT: '8095', STATIC_PORT: '8096', DOC_PORT: '8100', DOC_WORKER_COUNT: '5', PORT: '0', + APP_UNTRUSTED_URL : "http://localhost:18096", })), // This skips type-checking when running server, but reduces startup time a lot. TS_NODE_TRANSPILE_ONLY: 'true', @@ -158,6 +167,7 @@ export class TestServerMerged implements IMochaServer { // Prepare testingHooks for certain behind-the-scenes interactions with the server. this.testingHooks = await connectTestingHooks(testingSocket); + this.emit('start'); } public async stop() { @@ -168,6 +178,7 @@ export class TestServerMerged implements IMochaServer { this.testingHooks.close(); } await this._exitPromise; + this.emit('stop'); } /** @@ -261,7 +272,7 @@ export class TestServerMerged implements IMochaServer { } // substitute a custom driver - public setDriver(customDriver: WebDriver = driver) { + public setDriver(customDriver?: WebDriver) { this._driver = customDriver; } diff --git a/test/nbrowser/webdriverjq-nbrowser.js b/test/nbrowser/webdriverjq-nbrowser.js new file mode 100644 index 00000000..695f4daf --- /dev/null +++ b/test/nbrowser/webdriverjq-nbrowser.js @@ -0,0 +1,554 @@ +/** + * This is webdriverjq, modified to in fact no longer + * use jquery, but rather to work in conjunction with + * mocha-webdriver. Works in conjunction with + * gristUtils-nbrowser. + * + * Not everything webdriverjq could do is supported, + * just enough to make porting tests easier. The + * promise manager mechanism selenium used to have + * that old tests depends upon is gone (and good + * riddance). + * https://github.com/SeleniumHQ/selenium/blob/trunk/javascript/node/selenium-webdriver/CHANGES.md#api-changes-2 + * + * Changes are minimized here, no modernization unless + * strictly needed, so diff with webdriverjq.js is + * easier to read. + */ + +var _ = require('underscore'); +var util = require('util'); + +import { driver, error, stackWrapFunc, + WebElement, WebElementPromise } from 'mocha-webdriver'; + +export const driverCompanion = { + $: null, +}; + +export function webdriverjqWrapper(driver) { + + /** + * Returns a new WebdriverJQ instance. + */ + function $(selector) { + return new WebdriverJQ($, selector); + } + + $.driver = driver; + + $.getPage = function(url) { + return $.driver.get(url); + }; + + return $; +} + +/** + * The object obtained by $(...) calls. It supports nearly all the JQuery and WebElement methods, + * and allows chaining whenever it makes sense. + * @param {JQ} $: The $ object used to create this instance. + * @param {String|WebElement} selector: A string selector, or a WebElement (or promise for one). + */ +function WebdriverJQ($, selector) { + this.$ = $; + this.driver = $.driver; + this.selector = selector; + this._selectorDesc = selector; + this._callChain = []; +} + +/** + * $(...).method(...) invokes the corresponding method on the client side. Methods may be chained. + * In reality, method calls aren't performed until the first non-chainable call. For example, + * $(".foo").parent().toggleClass("active").text() is translated to a single call. + * + * Note that a few methods are overridden later: click() and submit() are performed on the server + * as WebDriver actions instead of on the client. + * + * The methods listed below are all the methods from http://api.jquery.com/. + */ +var JQueryMethodNames = [ + "add", "addBack", "addClass", "after", "ajaxComplete", "ajaxError", "ajaxSend", "ajaxStart", + "ajaxStop", "ajaxSuccess", "andSelf", "animate", "append", "appendTo", "attr", "before", "bind", + "blur", "change", "children", "clearQueue", "click", "clone", "closest", "contents", + "contextmenu", "css", "data", "dblclick", "delay", "delegate", "dequeue", "detach", "die", + "each", "empty", "end", "eq", "error", "fadeIn", "fadeOut", "fadeTo", "fadeToggle", "filter", + "find", "finish", "first", "focus", "focusin", "focusout", "get", "has", "hasClass", "height", + "hide", "hover", "html", "index", "innerHeight", "innerWidth", "insertAfter", "insertBefore", + "is", "keydown", "keypress", "keyup", "last", "live", "load", "load", "map", "mousedown", + "mouseenter", "mouseleave", "mousemove", "mouseout", "mouseover", "mouseup", "next", "nextAll", + "nextUntil", "not", "off", "offset", "offsetParent", "on", "one", "outerHeight", "outerWidth", + "parent", "parents", "parentsUntil", "position", "prepend", "prependTo", "prev", "prevAll", + "prevUntil", "promise", "prop", "pushStack", "queue", "ready", "remove", "removeAttr", + "removeClass", "removeData", "removeProp", "replaceAll", "replaceWith", "resize", "scroll", + "scrollLeft", "scrollTop", "select", "serialize", "serializeArray", "show", "siblings", + "slice", "slideDown", "slideToggle", "slideUp", "stop", "submit", "text", "toArray", "toggle", + "toggle", "toggleClass", "trigger", "triggerHandler", "unbind", "undelegate", "unload", + "unwrap", "val", "width", "wrap", "wrapAll", "wrapInner", + + // Extra methods (defined below, injected into client, available to these tests only). + "trimmedText", "classList", "subset", "filterExactText", "filterText", "getAttribute", + + "findOldTimey", // nbrowser: added +]; + +// Maps method name to an array of argument types. If called with matching types, this method does +// NOT return another JQuery object (i.e. it's not a chainable call). +var nonChainableMethods = { + "attr": ["string"], + "css": ["string"], + "data": ["string"], + "get": null, + "hasClass": ["string"], + "height": [], + "html": [], + "index": null, + "innerHeight": [], + "innerWidth": [], + "is": null, + "offset": [], + "outerHeight": [], + "outerWidth": [], + "position": [], + "prop": ["string"], + "scrollLeft": [], + "scrollTop": [], + "serialize": null, + "serializeArray": null, + "text": [], + "toArray": null, + "val": [], + "width": [], + + "trimmedText": null, + "classList": null, + "getAttribute": null, +}; + +function isNonChainable(methodName, args) { + var argTypes = nonChainableMethods[methodName]; + return argTypes === null || + _.isEqual(argTypes, args.map(function(a) { return typeof a; })); +} + +JQueryMethodNames.forEach(function(methodName) { + WebdriverJQ.prototype[methodName] = function() { + var args = Array.prototype.slice.call(arguments, 0); + var methodCallInfo = [methodName].concat(args); + var newObj = this.clone(); + newObj._callChain.push(methodCallInfo); + if (isNonChainable(methodName, args)) { + return newObj.resolve(); + } else { + return newObj; + } + }; +}); + +// .length property is a special case. +Object.defineProperty(WebdriverJQ.prototype, 'length', { + configurable: false, + enumerable: false, + get: function() { + var newObj = this.clone(); + newObj._callChain.push(['length']); + return newObj.resolve(); + } +}); + + +/** + * WebdriverJQ objects also support various WebDriver.WebElement methods, namely the ones + * below. These operate on the first of the selected elements. Those without a meaningful return + * value may be chained. E.g. $(".foo").click().val() will trigger a click on the selected + * element, then return its 'value' property. + */ +// This maps each supported method name to whether or not it should be chainable. +var WebElementMethodNames = { + "getId": false, + "getRawId": false, + "click": true, + "sendKeys": true, + "getTagName": false, + "getCssValue": false, + "getText": false, + "getSize": false, + "getLocation": false, + "isEnabled": false, + "isSelected": false, + "submit": true, + "clear": true, + "isDisplayed": false, + "isPresent": false, // nbrowser: added + "getOuterHtml": false, + "getInnerHtml": false, + "scrollIntoView": true, + "sendNewText": true, // Added below. +}; + +Object.keys(WebElementMethodNames).forEach(function(methodName) { + function runMethod(self, methodName, elem, ...argList) { + var result = elem[methodName].apply(elem, argList) + .then(function(value) { + // Chrome makes some values unprintable by including a bogus .toString property. + if (value && typeof value.toString !== 'function') { delete value.toString; } + return value; + }, function(err) { + throw err; + }); + return result; + } + const runThisMethod = stackWrapFunc(runMethod.bind(null, this, methodName)); + WebdriverJQ.prototype[methodName] = function() { + const elem = this.elem(); + const result = runThisMethod(elem, ...arguments); + if (WebElementMethodNames[methodName]) { + // If chainable, create a new WebdriverJQ object (that waits for the result). + return this._chain(() => elem, result); + } else { + // If not a chainable method, then we are done. + return result; + } + }; +}); + + +/** + * Helper for chaining. Creates a new WebdriverJQ instance from the current one for the given + * element, but which resolves only when the given promise resolves. + */ +WebdriverJQ.prototype._chain = function(elemFn, optPromise) { + const getElemAndUpdateDesc = () => { + const elem = elemFn(); + // Let the chained object start with the previous object's description, but once we have + // resolved the element, update it with the resolved element. + chainable._selectorDesc = this._selectorDesc + " [pending]"; + elem.then(function(resolvedElem) { + chainable._selectorDesc = resolvedElem.toString(); + }, function(err) {}); + return elem; + }; + var chainable = new WebdriverJQ(this.$, + optPromise ? optPromise.then(getElemAndUpdateDesc) : getElemAndUpdateDesc()); + + return chainable; +}; + + +/** + * Return a friendly string representation of this WebdriverJQ instance. + * E.g. $('.foo').next() will be represented as "$('.foo').next()". + */ +WebdriverJQ.prototype.toString = function() { + var sel = this._selectorDesc; + if (typeof sel === 'string') { + sel = "'" + sel.replace(/'/g, "\\'") + "'"; + } else { + sel = sel.toString(); + } + var desc = "$(" + sel + ")"; + desc += this._callChain.map(function(methodCallInfo) { + var method = methodCallInfo[0], args = util.inspect(methodCallInfo.slice(1)); + return "." + method + "(" + args.slice(1, args.length - 1).trim() + ")"; + }).join(""); + return desc; +}; + + +/** + * Returns a copy of the WebdriverJQ object. + */ +WebdriverJQ.prototype.clone = function() { + var newObj = new WebdriverJQ(this.$, this.selector); + newObj._selectorDesc = this._selectorDesc; + newObj._callChain = this._callChain.slice(0); + return newObj; +}; + + +/** + * Convert the matched elements to an array and apply all further chained calls to each elemet of + * the array separately, so that the result will be an array. + * + * E.g. $(".foo").array().text() will return an array of text for each of the elements matching + * ".foo", and $(".foo").array().height() will return an array of heights (unlike + * $(".foo").height() which would return the height of the first matching element). + */ +WebdriverJQ.prototype.array = function() { + this._callChain.push(["array"]); + return this; +}; + + +/** + * Make the call to the browser, returning a promise for the values (typically an array) returned + * by the browser. + */ +WebdriverJQ.prototype.resolve = function() { + var self = this; + if (isPromise(this.selector)) { + // Update our selector description once we know what it resolves to. + this.selector.then(function(resolvedSelector) { + self._selectorDesc = resolvedSelector.toString(); + }, function(err) {}); + + if (this._callChain.length === 0) { + // If the selector is a promise and there are no chained calls, there is no need to execute + // anything, we just need for the promise to resolve. + return this.selector.then(function(value) { + return Array.isArray(value) ? value : [value]; + }); + } + } + + return executeChain(this.selector, this._callChain); +}; + + +/** + * Make a call to the browser now, expecting a single element returned, and returning + * WebElementPromise for that element. If no elements match, the promise will be rejected. + */ +WebdriverJQ.prototype.elem = function() { + const doElem = stackWrapFunc(() => { + var self = this; + // TODO: we could limit results to a single value for efficiency. + var result = this.resolve().then(function(elems) { + if (!elems[0]) { throw new Error(self + " matched no element"); } + return elems[0]; + }); + return result; + }); + return new WebElementPromise(driver, doElem()); +}; + + +/** + * Check if the element is considered stale by WebDriver. An element is considered stale once it + * is removed from the DOM, or a new page has loaded. + */ +WebdriverJQ.prototype.isStale = function() { + return this.getTagName().then(function() { return false; }, function(err) { + if (err instanceof error.StaleElementReferenceError) { + return true; + } + throw err; + }); +}; + +/** + * Helper that allows a WebdriverJQ to act as a promise, but really just forwarding calls to + * this.resolve().then(...) + */ +WebdriverJQ.prototype.then = function(success, failure) { + // In selenium-webdriver 2.46, it was important not to call this.resolve().then(...) directly, + // but to start a new promise chain, to avoid a deadlock in ControlFlow. In selenium-webdriver + // 2.48, starting a new promise chain is wrong since the webdriver doesn't wait for the new + // promise chain on errors, and fails without a chance to catch them. + return this.resolve().then(success, failure); +}; + +// webdriver.promise.Thenable.addImplementation(WebdriverJQ); + + +/** + * Wait for a condition, represented by func(elem) returning true. The function may use asserts to + * indicate that the condition isn't met yet. E.g. + * + * $(...).wait().click() // Wait for a matching element to be present, then click it. + * $(...).wait(assert.isPresent).click() // Equivalent to the previous line. + * $(...).wait(assert.isDisplayed) // Wait for the matching element to be displayed. + * $(...).wait(assert.isDisplayed, false) // Wait for the element to NOT be displayed. + * $(...).wait(assert.hasClass, 'foo') // Wait for the element to have class 'foo' + * $(...).wait(assert.hasClass, 'foo', false) // Wait for the element to NOT have class 'foo' + * + * @param [Number] optTimeoutMs: First numerical argument is interpreted as a timeout in seconds. + * Default is 10. You may pass in a longer timeout, but infinite wait isn't supported. + * @param [Function] func: Optional condition function called as func(wjq, args...), where `wjq` + * is a WebdriverJQ instance. If a string is given, then `wjq[func](args...)` is called + * instead. If omitted, wait until the selector matches at least one element. + * The function must not return undefined, but may throw chai.AssertionError. + * @param [Objects] args...: Optional additional arguments to pass to func. + * @returns WebdriverJQ, which may be chained further. + */ +WebdriverJQ.prototype.waitCore = function(chained, optTimeoutSec, func, ...extraArgs) { + var timeoutMs; + if (typeof optTimeoutSec === 'number') { + timeoutMs = optTimeoutSec * 1000; + if (arguments.length === 2) { func = null; } + } else { + timeoutMs = 10000; + extraArgs.unshift(func); + func = (arguments.length === 1) ? null : optTimeoutSec; + } + if (_.isUndefined(func)) { + var failed = Promise.reject( + new Error("WebdriverJQ: wait called with undefined condition")); + return this._chain(() => failed); + } + func = func || "isPresent"; + + var self = this; + async function conditionFunc() { + const result = await (typeof func === 'string' ? + self[func].apply(self, extraArgs) : + func.apply(null, [self].concat(extraArgs))); + return result === undefined ? true : result; + } + var waitPromise = waitImpl(timeoutMs, conditionFunc); + return chained ? this._chain(() => this.resolve(), waitPromise) : + waitPromise; +}; + +WebdriverJQ.prototype.wait = function(...args) { + return this.waitCore(true, ...args); +} + +WebdriverJQ.prototype.waitDrop = function(...args) { + return this.waitCore(false, ...args); +} + + +/** + * Send keyboard keys to the element as if typed by the user. This allows the use of arrays as + * arguments to scope modifier keys. The method allows chaining. + */ +WebdriverJQ.prototype.sendKeys = function(varKeys) { + var keys = processKeys(Array.prototype.slice.call(arguments, 0)); + var elem = this.elem(); + return this._chain(() => elem, elem.sendKeys.apply(elem, keys)); +}; + +/** + * Replaces the value in a text by sending the keys to select all text, type + * the given string, and hit Enter. + */ +WebdriverJQ.prototype.sendNewText = function(string) { + return this.sendKeys(this.$.SELECT_ALL || driverCompanion.$.SELECT_ALL, + string, + this.$.ENTER || driverCompanion.$.ENTER); +}; + +/** + * Transform the given array of keys to prepare for sending to the browser: + * (1) If an argument is an array, its elements are grouped using webdriver.Key.chord() + * (in other words, modifier keys present in the array are released after the array). + * (2) Transforms certain keys to work around a selenium bug where they don't get processed + * properly. + */ +function processKeys(keyArray) { + return keyArray.map(function(arg) { + if (Array.isArray(arg)) { + arg = driver.Key.chord.apply(driver.Key, arg); + } + return arg; + }); +} + +//---------------------------------------------------------------------- +// Enhancements to webdriver's own objects. +//---------------------------------------------------------------------- +WebElement.prototype.toString = function() { + if (this._description) { + return "<" + this._description + ">"; + } else { + return ""; + } +}; + +//---------------------------------------------------------------------- +// "Client"-side code. +//---------------------------------------------------------------------- + +// Run a command chain. Basically just find the initial element, and +// apply methods to it. We try to match quirks of old system, which +// are a bit hard to explain, and can't easily be matched exactly - +// but well enough to cover a lot of test code it seems (and the +// rest can just be rewritten). +async function executeChain(selector, callChain) { + const cc = callChain.map(c => c[0]); + let result = selector; + if (typeof selector === 'string') { + result = await findOldTimey(driver, selector, + cc.includes('array') || + cc.includes('length') || + cc.includes('toArray') || + cc.includes('eq') || + cc.includes('last')); + } + result = await applyCallChain(callChain, result); + if (result instanceof WebElement) { + result = [result]; + } + return result; +} + +async function applyCallChain(callChain, value) { + value = await value; + for (var i = 0; i < callChain.length; i++) { + var method = callChain[i][0], args = callChain[i].slice(1).map(translateTestId); + if (method === "toArray") { + return Promise.all(value); + } else if (method === "array") { + return Promise.all(value.map(applyCallChain.bind(null, callChain.slice(i + 1)))); + } else if (method === "last") { + return applyCallChain(callChain.slice(i + 1), value[value.length - 1]); + } else if (method === "eq") { + const idx = args[0]; + return applyCallChain(callChain.slice(i + 1), value[idx]); + } else if (method === "length") { + return value.length; + } else { + if (!value[method] && value[0]?.[method]) { + value = value[0]; + } + value = await value[method].apply(value, args); + } + } + return value; +} + +function translateTestId(selector) { + return (typeof selector === 'string' ? + selector.replace(/\$(\w+)/, '[data-test-id="$1"]', 'g') : + selector); +} + +export function findOldTimey(obj, key, multiple, multipleOptions) { + key = translateTestId(key); + const contains = key.split(':contains'); + if (contains.length === 2) { + const content = contains[1].replace(/["'()]/g, ''); + return obj.findContent(contains[0], content); + } + if (multiple) { + return obj.findAll(key, multipleOptions); + } + return obj.find(key); +} + +// Similar to gu.waitToPass (copied to simplify import structure). +export async function waitImpl(timeoutMs, conditionFunc) { + try { + await driver.wait(async () => { + try { + return await conditionFunc(); + } catch (e) { + return false; + } + }, timeoutMs); + } catch (e) { + await conditionFunc(); + } +} + +function isPromise(obj) { + if (typeof obj !== 'object') { + return false; + } + if (typeof obj['then'] !== 'function') { + return false; + } + return true; +} diff --git a/test/nbrowser/webdriverjq.ntest.js b/test/nbrowser/webdriverjq.ntest.js new file mode 100644 index 00000000..977afd71 --- /dev/null +++ b/test/nbrowser/webdriverjq.ntest.js @@ -0,0 +1,215 @@ +import { assert, driver } from 'mocha-webdriver'; +import { $, gu, server, test } from 'test/nbrowser/gristUtil-nbrowser'; + +/** + * Not much of the fancy list support of webdriverjq has been supported. + * Luckily not many of the tests needed it, and the parts that did have + * been rewritten. So most of this test is turned off, and is kept just + * for reference purposes. + */ + +describe("webdriverjq.ntest", function() { + test.setupTestSuite(this); + + before(async function() { + await gu.supportOldTimeyTestCode(); + await driver.get(server.getHost() + '/v/gtag/testWebdriverJQuery.html'); + }); + + it("should support basic jquery syntax", async function() { + // toString should work properly. + assert.equal("" + $("input[type='button']"), "$('input[type=\\'button\\']')"); + + assert.equal(await $(".foo").trimmedText(), "Hello world"); + assert.equal((await $(".bar").array()).length, 2); + assert.lengthOf(await $(".bar").toArray(), 2); + assert(await $(".foo").hasClass("bar")); + assert.equal(await $(".foo.bar .baz").parent().getAttribute('className'), "foo bar"); + // Can't quite match old property-over-list behavior. + // assert.equal(await $(".foo.bar").find(".baz").parent().prop('className'), "foo bar"); + // Parent behavior is not the same as it was. + // assert.equal(await $(".baz").parent().length, 2); + assert.equal(await $(".baz input").val(), "Go"); + // There are two bazs, in new style need to specify which. + assert.equal(await $(".baz").eq(1).find('input[type="button"]').val(), "Go"); + await $("input[type='button']").click(); + assert.equal(await $(".baz input").val(), "Goo"); + await $(".baz input").val("Go").resolve(); // Revert the value to avoid affecting other test cases. + // toggleClass not supported anymore. + // assert.notInclude(await $(".foo").toggleClass("bar").classList(), "bar"); + // assert.include(await $(".foo").toggleClass("bar").classList(), "bar"); + }); + + it("should support .array()", async function() { + assert.deepEqual(await $(".bar").getAttribute('className'), 'foo bar'); + assert.deepEqual(await $(".bar").array().getAttribute('className'), ['foo bar', 'bar']); + assert.deepEqual(await $(".bar").array().trimmedText(), ["Hello world", "Good bye"]); + assert.deepEqual(await $(".bar").eq(0).trimmedText(), "Hello world"); + assert.deepEqual(await $(".bar").eq(1).trimmedText(), "Good bye"); + }); + + it("should support WebElement methods and chaining", async function() { + assert.equal(await $(".baz").getText(), "Hello world"); + assert.equal(await $(".baz").getAttribute("class"), "baz"); + await $(".baz").click(); + + // Cannot chain with clicks anymore + // assert.equal($(".baz").click().getText(), "Hello world"); + // assert.equal($(".baz").click().trimmedText(), "Hello world"); + // assert.equal($(".baz").click().parent().prop("className"), "foo bar"); + // assert.equal($(".baz").click().parent().isDisplayed(), true); + + // Errors are different. + // assert.equal(await $(".nonexistent1").text(), ""); + await assert.isRejected($(".nonexistent2").getText(), /no such element/); + await assert.isRejected($(".nonexistent3").click(), /no such element/); + await assert.isRejected($(".nonexistent4").isDisplayed(), /no such element/); + await assert.isRejected($(".nonexistent5").click().parent().isDisplayed(), /no such element/); + + await assert.isRejected($(".foo").click().find(".bar").elem(), /no such element/); + + // cannot chain click anymore + // assert($(".foo").click().find(".baz").elem()); + // assert.lengthOf($(".foo").click().find(".baz"), 1); + // assert.lengthOf($(".foo").click().find(".bar"), 0); + assert.lengthOf(await $(".bar").array(), 2); + await $(".bar").array().resolve().then(function(elems) { assert.lengthOf(elems, 2); }); + }); + + function expectFailure(promise, regexp) { + throw new Error('not ported'); + /* + var stack = stacktrace.captureStackTrace("", expectFailure); + return stacktrace.resolveWithStack(stack, promise.then(function(value) { + throw new Error("Expected failure but got " + value); + }, function(err) { + assert.match(err.message, regexp); + // Also make sure that our filename is present in the stack trace. + assert.match(err.stack, /webdriverjq.test.js:\d+/); + })); + */ + } + + // Custom asserts work, but error messages are different and not + // very interesting to maintain. + it.skip("should work with our custom asserts", async function() { + await assert.hasClass($(".foo"), "bar"); + await expectFailure(assert.hasClass($(".foo"), "bar", false), /hasClass/); + + await assert.hasClass($(".foo"), "xbar", false); + await expectFailure(assert.hasClass($(".foo"), "xbar"), /hasClass/); + + assert.isEnabled($("#btn")); + expectFailure(assert.isEnabled($("#btn"), false), /isEnabled/); + + assert.isEnabled($("#btn").prop("disabled", true), false); + expectFailure(assert.isEnabled($("#btn"), true), /isEnabled/); + + assert.isEnabled($("#btn").prop("disabled", false), true); + + assert.isPresent($("#btn")); + expectFailure(assert.isPresent($("#btnx")), /isPresent/); + + assert.isPresent($("#btnx"), false); + expectFailure(assert.isPresent($("#btn"), false), /isPresent/); + + assert.isDisplayed($("#btn")); + expectFailure(assert.isDisplayed($("#btn"), false), /isDisplayed/); + + assert.isDisplayed($(".baz").css('display', 'none').find("#btn"), false); + expectFailure(assert.isDisplayed($("#btn")), /isDisplayed/); + expectFailure(assert.ok($("#btn").click()), /not interactable/); + + assert.isDisplayed($(".baz").css('display', '').find("#btn"), true); + expectFailure(assert.isDisplayed($("#btn"), false), /isDisplayed/); + }); + + it.skip("should report good errors", async function() { + await $(".baz").css('display', 'none').resolve(); + expectFailure(assert.ok($("#btn").click()), /not interactable/); + await $(".baz").css('display', '').resolve(); + assert.ok($("#btn").click()); + assert.equal($("#btn").val(), "Goo"); + await $("#btn").val("Go").resolve(); // Revert the value to avoid affecting other test cases. + + expectFailure($(".nonexistent1").click(), /nonexistent1.* matched no element/); + expectFailure(assert.ok($(".nonexistent2").click()), /matched no element/); + expectFailure($(".nonexistent3").getText(), /matched no element/); + expectFailure(assert.ok($(".nonexistent5").elem()), /matched no element/); + }); + + // addClass not supported anymore. + it.skip("should wait for various conditions", async function() { + assert.equal(await $(".foo").wait().trimmedText(), "Hello world"); + + // Test waits for functions of an existing element. + await driver.executeScript(function() { + setTimeout(function() { $(".foo .baz").addClass("later1"); }, 300); + setTimeout(function() { $(".foo .baz").addClass("later2"); }, 700); + }); + assert.deepEqual(await $(".foo .baz").classList(), ["baz"]); + await assert.hasClass($(".foo .baz"), "later2", false); + assert.deepEqual(await $(".foo .baz").wait(assert.hasClass, "later1").classList(), + ["baz", "later1"]); + assert.deepEqual(await $(".foo .baz").wait("hasClass", "later2").classList(), + ["baz", "later1", "later2"]); + assert.throws($(".foo .baz").wait(0.05, "hasClass", "never").classList(), + /Wait timed out/); + + // Test waits for the presence of an element. + $.driver.executeScript(async function() { + await $(".foo .baz").removeClass("later1 later2"); + setTimeout(function() { $(".foo .baz").addClass("later1"); }, 200); + setTimeout(function() { $(".foo .baz").addClass("later2"); }, 500); + setTimeout(function() { $(".foo .baz").removeClass("later1 later2"); }, 1000); + }); + assert.lengthOf($(".later1, .later2"), 0); + assert.throws($(".later1").wait(0.05, "isPresent").classList(), /Wait timed out/); + assert.deepEqual($(".later1").wait().classList(), ["baz", "later1"]); + assert.deepEqual($(".later2").wait(1, assert.isPresent).classList(), + ["baz", "later1", "later2"]); + + // The element is already present, so this should be true. + assert.equal($(".later1").wait(0.01, assert.isPresent, true).isPresent(), true); + // The following is equivalent to WebDrivers's until.stalenessOf. + assert.equal($(".later1").wait(1, assert.isPresent, false).isPresent(), false); + + // Absent argument, or null, are OK, and mean "isPresent", but 'undefined' as an argument is a + // liability, since it would be silent and wrong on misspellings. So we catch it. + assert.equal($(".foo").wait(null).isPresent(), true); + assert.throws($(".foo").wait(assert.misspelled).isPresent(), + /called with undefined condition/); + + // We should be able to chain beyond .wait() with actions and more. + $.driver.executeScript(function() { + setTimeout(function() { $("#btn").addClass("later1"); }, 200); + setTimeout(function() { $("#btn").removeClass("later1"); }, 800); + }); + await $("#btn.later1").wait().click(); + assert.equal($("#btn").val(), "Goo"); + await $("#btn").wait(assert.hasClass, "later1", false).click(); + assert.equal($("#btn").val(), "Gooo"); + await $("#btn").val("Go").resolve(); // Revert the value to avoid affecting other test cases. + }); + + // behavior around lists changed. + it.skip("should support complicated promises", async function() { + var elemA = $(".foo .baz").resolve().then(function(elem) { + return $(elem).parent(); + }).then(function(elem) { + assert.deepEqual($(elem).classList(), ["foo", "bar"]); + return elem; + }); + + assert.deepEqual($(elemA).classList(), ["foo", "bar"]); + assert.isDisplayed($(elemA)); + assert.isDisplayed($(elemA).find(".baz")); + assert.throws($(elemA).find(".nonexistent").click(), / { - if (noexit) { - console.log("report-why-tests-hang silenced with --no-exit flag"); - } else { - // If still hanging after 5s after tests finish, say something. Unref() ensures that THIS - // timeout doesn't itself keep node from exiting. - setTimeout(report, 5000).unref(); +if (process.env.MOCHA_WORKER_ID === undefined) { + exports.mochaHooks = { + afterAll(done) { + if (noexit) { + console.log("report-why-tests-hang silenced with --no-exit flag"); + } else { + // If still hanging after 5s after tests finish, say something. Unref() ensures that THIS + // timeout doesn't itself keep node from exiting. + setTimeout(report, 5000).unref(); + } + done(); + } } -}); +} diff --git a/test/server/lib/DocApi.ts b/test/server/lib/DocApi.ts index 2c56b065..c28fc080 100644 --- a/test/server/lib/DocApi.ts +++ b/test/server/lib/DocApi.ts @@ -3022,10 +3022,9 @@ function testDocApi() { }); after(async function () { - if (!process.env.TEST_REDIS_URL) { - this.skip(); + if (process.env.TEST_REDIS_URL) { + await redisClient.quitAsync(); } - await redisClient.quitAsync(); }); }); @@ -3260,10 +3259,9 @@ function testDocApi() { }); after(async function () { - if (!process.env.TEST_REDIS_URL) { - this.skip(); + if (process.env.TEST_REDIS_URL) { + await redisMonitor.quitAsync(); } - await redisMonitor.quitAsync(); }); it("delivers expected payloads from combinations of changes, with retrying and batching", async function () { diff --git a/test/setupPaths.js b/test/setupPaths.js index f579a663..e1e8ba21 100644 --- a/test/setupPaths.js +++ b/test/setupPaths.js @@ -1,7 +1,17 @@ // enhance require() to support project paths and typescript. const path = require('path'); const appModulePath = require('app-module-path'); -const root = path.dirname(__dirname); -appModulePath.addPath(path.join(root, "_build")); -appModulePath.addPath(path.join(root, "_build/core")); -appModulePath.addPath(path.join(root, "_build/ext")); +// Root path can be complicated, pwd is more reliable for tests. +const root = process.cwd(); +const nodePath = (process.env.NODE_PATH || '').split(path.delimiter); +const paths = [path.join(root, "_build"), + path.join(root, "_build/core"), + path.join(root, "_build/ext"), + path.join(root, "_build/stubs")]; +for (const p of paths) { + appModulePath.addPath(p); +} +// add to path for any subprocesses also +process.env.NODE_PATH = [...nodePath, ...paths] + .filter(p => p !== '') + .join(path.delimiter); diff --git a/test/split-tests.js b/test/split-tests.js index 718875b6..144aa263 100644 --- a/test/split-tests.js +++ b/test/split-tests.js @@ -18,36 +18,38 @@ * parallel groups can be evened out as much as possible. */ -/* global before */ const fs = require('fs'); const { assert } = require('chai'); const testSuite = process.env.TEST_SUITE_FOR_TIMINGS || "unset_suite"; const timingsFile = process.env.TIMINGS_FILE || "test/timings-all.txt"; -before(function() { - const testSplits = process.env.TEST_SPLITS; - if (!testSplits) { - return; - } - const match = testSplits.match(/^(\d+)-of-(\d+)$/); - if (!match) { - assert.fail(`Invalid test split spec '${testSplits}': use format 'N-of-M'`); - } +exports.mochaHooks = { + beforeAll(done) { + const testSplits = process.env.TEST_SPLITS; + if (!testSplits) { + return done(); + } + const match = testSplits.match(/^(\d+)-of-(\d+)$/); + if (!match) { + assert.fail(`Invalid test split spec '${testSplits}': use format 'N-of-M'`); + } - const group = Number(match[1]); - const groupCount = Number(match[2]); - if (!(group >= 1 && group <= groupCount)) { - assert.fail(`Invalid test split spec '${testSplits}': index must be in range 1..{groupCount}`); - } + const group = Number(match[1]); + const groupCount = Number(match[2]); + if (!(group >= 1 && group <= groupCount)) { + assert.fail(`Invalid test split spec '${testSplits}': index must be in range 1..{groupCount}`); + } - const testParent = this.test.parent; - const timings = getTimings(); - const groups = groupSuites(testParent.suites, timings, groupCount); + const testParent = this.test.parent; + const timings = getTimings(); + const groups = groupSuites(testParent.suites, timings, groupCount); - testParent.suites = groups[group - 1]; // Convert to a 0-based index. - console.log(`Split tests groups; will run group ${group} of ${groupCount}`); -}); + testParent.suites = groups[group - 1]; // Convert to a 0-based index. + console.log(`Split tests groups; will run group ${group} of ${groupCount}`); + done(); + } +}; /** * Read timings from timingsFile into a Map mapping file-suite-title to duration. diff --git a/test/utils.js b/test/utils.js index 1489fcbc..643d6bea 100644 --- a/test/utils.js +++ b/test/utils.js @@ -1,4 +1,4 @@ -/* global location, describe, it, afterEach, after */ +/* global location */ var _ = require('underscore'); var Chance = require('chance'); diff --git a/test/xunit-file.js b/test/xunit-file.js index 0e18ba81..a6c8e3ed 100644 --- a/test/xunit-file.js +++ b/test/xunit-file.js @@ -69,7 +69,7 @@ class XUnitFile extends reporters.Base { runner.on('suite end', (suite) => { // Every time a (top-level) suite ends, add a line to the timings file. - if (suite.titlePath().length == 1) { + if (suite.titlePath?.()?.length == 1) { const duration = Date.now() - startedSuites.get(suite); appendLine(timingsFd, `${testSuite} ${suite.fullTitle()} ${duration}`); startedSuites.delete(suite); diff --git a/yarn.lock b/yarn.lock index 1ab91736..23b597a1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -505,6 +505,11 @@ 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" @@ -551,6 +556,13 @@ 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" @@ -592,6 +604,16 @@ "@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" @@ -706,6 +728,11 @@ 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-proxy@1.17.9": version "1.17.9" resolved "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.9.tgz" @@ -765,6 +792,13 @@ 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" @@ -876,6 +910,13 @@ "@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/saml2-js@2.0.1": version "2.0.1" resolved "https://registry.npmjs.org/@types/saml2-js/-/saml2-js-2.0.1.tgz" @@ -1323,10 +1364,10 @@ 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.3: - version "0.5.3" - resolved "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.3.tgz" - integrity sha512-zsoTXEwRNCxBzRHLENFLuecCcwzzXiEhWo1r3GP68iwi8Q/hW2RrqgeY1nfJ/AhNQNWnZq/4v0TbfMsUkI+TYw== +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" @@ -1374,15 +1415,10 @@ ansi-align@^3.0.0: dependencies: string-width "^3.0.0" -ansi-colors@3.2.3: - version "3.2.3" - resolved "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz" - integrity sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw== - -ansi-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz" - integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= +ansi-colors@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" + integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== ansi-regex@^4.1.0: version "4.1.0" @@ -1399,7 +1435,7 @@ ansi-regex@^5.0.1: resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== -ansi-styles@^3.2.0, ansi-styles@^3.2.1: +ansi-styles@^3.2.1: version "3.2.1" resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz" integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== @@ -1439,6 +1475,14 @@ anymatch@~3.1.1: normalize-path "^3.0.0" picomatch "^2.0.4" +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + app-module-path@2.2.0: version "2.2.0" resolved "https://registry.npmjs.org/app-module-path/-/app-module-path-2.2.0.tgz" @@ -1820,6 +1864,13 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + brace@0.11.1: version "0.11.1" resolved "https://registry.npmjs.org/brace/-/brace-0.11.1.tgz" @@ -2094,6 +2145,11 @@ 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" @@ -2107,6 +2163,19 @@ 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" @@ -2130,6 +2199,11 @@ camelcase@^5.0.0, camelcase@^5.3.1: resolved "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== +camelcase@^6.0.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + caniuse-lite@^1.0.30001358: version "1.0.30001359" resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001359.tgz" @@ -2140,11 +2214,6 @@ caniuse-lite@^1.0.30001400: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001441.tgz#987437b266260b640a23cd18fbddb509d7f69f3e" integrity sha512-OyxRR4Vof59I3yGWXws6i908EtGbMzVUi3ganaZQHmydk1iwDhRnvaPG2WaR0KcqrDFKrxVZHULT396LEPhXfg== -capture-stack-trace@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.1.tgz" - integrity sha512-mYQLZnx5Qt1JgB1WEiMCf2647plpGeQ2NMR/5L0HNZzGQo4fuSPnK+wjfPnKZV0aiJDgzmWqqkV/g7JD+DW0qw== - caseless@~0.12.0: version "0.12.0" resolved "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz" @@ -2203,7 +2272,7 @@ chainsaw@~0.1.0: dependencies: traverse ">=0.3.0 <0.4" -chalk@^2.0.0, chalk@^2.4.2: +chalk@^2.0.0: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -2246,20 +2315,20 @@ check-error@^1.0.2: resolved "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz" integrity sha1-V00xLt2Iu13YkS6Sht1sCu1KrII= -chokidar@3.3.0: - version "3.3.0" - resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.3.0.tgz" - integrity sha512-dGmKLDdT3Gdl7fBUe8XK+gAtGmzy5Fn0XkkWQuYxGIgWVPPse2CxFA5mtrlD0TOHaHjEUqkWNyP1XdHoJES/4A== +chokidar@3.5.3: + version "3.5.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== dependencies: - anymatch "~3.1.1" + anymatch "~3.1.2" braces "~3.0.2" - glob-parent "~5.1.0" + glob-parent "~5.1.2" is-binary-path "~2.1.0" is-glob "~4.0.1" normalize-path "~3.0.0" - readdirp "~3.2.0" + readdirp "~3.6.0" optionalDependencies: - fsevents "~2.1.1" + fsevents "~2.3.2" chokidar@^3.2.2: version "3.4.0" @@ -2334,15 +2403,6 @@ cli-highlight@^2.1.11: parse5-htmlparser2-tree-adapter "^6.0.0" yargs "^16.0.0" -cliui@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz" - integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA== - dependencies: - string-width "^3.1.0" - strip-ansi "^5.2.0" - wrap-ansi "^5.1.0" - cliui@^6.0.0: version "6.0.0" resolved "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz" @@ -2471,11 +2531,6 @@ combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" -commander@2.15.1: - version "2.15.1" - resolved "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz" - integrity sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag== - commander@9.3.0: version "9.3.0" resolved "https://registry.npmjs.org/commander/-/commander-9.3.0.tgz" @@ -2647,13 +2702,6 @@ create-ecdh@^4.0.0: bn.js "^4.1.0" elliptic "^6.0.0" -create-error-class@^3.0.1: - version "3.0.2" - resolved "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz" - integrity sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y= - dependencies: - capture-stack-trace "^1.0.0" - create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz" @@ -2800,20 +2848,6 @@ debug@2.6.9, debug@^2.2.0, debug@^2.6.0, debug@^2.6.9: dependencies: ms "2.0.0" -debug@3.1.0: - version "3.1.0" - resolved "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz" - integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== - dependencies: - ms "2.0.0" - -debug@3.2.6, debug@^3.2.6: - version "3.2.6" - resolved "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz" - integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== - dependencies: - ms "^2.1.1" - debug@4, debug@4.3.1: version "4.3.1" resolved "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz" @@ -2821,18 +2855,30 @@ debug@4, debug@4.3.1: dependencies: ms "2.1.2" -debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4: +debug@4.3.4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4: version "4.3.4" resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== dependencies: ms "2.1.2" +debug@^3.2.6: + version "3.2.6" + resolved "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz" + integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== + dependencies: + ms "^2.1.1" + decamelize@^1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz" integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= +decamelize@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" + integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== + decimal.js@^10.2.1: version "10.4.0" resolved "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.0.tgz" @@ -2850,6 +2896,13 @@ 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" @@ -2882,12 +2935,10 @@ 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== -define-properties@^1.1.2, define-properties@^1.1.3: - version "1.1.3" - resolved "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz" - integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== - dependencies: - object-keys "^1.0.12" +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" @@ -2990,7 +3041,12 @@ diff-match-patch@1.0.5: resolved "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz" integrity sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw== -diff@3.5.0, diff@^3.5.0: +diff@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" + integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== + +diff@^3.5.0: version "3.5.0" resolved "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz" integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== @@ -3057,7 +3113,7 @@ double-ended-queue@2.1.0-0, double-ended-queue@^2.1.0-0: resolved "https://registry.npmjs.org/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz" integrity sha1-ED01J/0xUo9AGIEwyEHv3XgmTlw= -duplexer2@^0.1.2, duplexer2@^0.1.4, duplexer2@~0.1.0, duplexer2@~0.1.2, duplexer2@~0.1.4: +duplexer2@^0.1.2, duplexer2@~0.1.0, duplexer2@~0.1.2, duplexer2@~0.1.4: version "0.1.4" resolved "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz" integrity sha1-ixLauHjA1p4+eJEFFmKjL8a93ME= @@ -3209,49 +3265,11 @@ err-code@^2.0.2: resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9" integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA== -error-ex@^1.2.0: - version "1.3.2" - resolved "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz" - integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== - dependencies: - is-arrayish "^0.2.1" - -es-abstract@^1.18.0-next.2: - version "1.18.0" - resolved "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0.tgz" - integrity sha512-LJzK7MrQa8TS0ja2w3YNLzUgJCGPdPOV1yVvezjNnS89D+VR08+Szt2mz3YB2Dck/+w5tfIq/RoUAFqJJGM2yw== - dependencies: - call-bind "^1.0.2" - es-to-primitive "^1.2.1" - function-bind "^1.1.1" - get-intrinsic "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.2" - is-callable "^1.2.3" - is-negative-zero "^2.0.1" - is-regex "^1.1.2" - is-string "^1.0.5" - object-inspect "^1.9.0" - object-keys "^1.1.1" - object.assign "^4.1.2" - string.prototype.trimend "^1.0.4" - string.prototype.trimstart "^1.0.4" - unbox-primitive "^1.0.0" - es-module-lexer@^0.9.0: version "0.9.3" resolved "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz" integrity sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ== -es-to-primitive@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz" - integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== - dependencies: - is-callable "^1.1.4" - is-date-object "^1.0.1" - is-symbol "^1.0.2" - es6-error@^4.1.1: version "4.1.1" resolved "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz" @@ -3410,16 +3428,16 @@ escape-html@~1.0.3: resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz" integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= -escape-string-regexp@1.0.5, escape-string-regexp@^1.0.5: +escape-string-regexp@4.0.0, escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= -escape-string-regexp@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" - integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== - escodegen@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz" @@ -3794,12 +3812,13 @@ finalhandler@1.1.1: statuses "~1.4.0" unpipe "~1.0.0" -find-up@3.0.0, find-up@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz" - integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== +find-up@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== dependencies: - locate-path "^3.0.0" + locate-path "^6.0.0" + path-exists "^4.0.0" find-up@^4.0.0, find-up@^4.1.0: version "4.1.0" @@ -3817,12 +3836,10 @@ flat-cache@^3.0.4: flatted "^3.1.0" rimraf "^3.0.2" -flat@^4.1.0: - version "4.1.1" - resolved "https://registry.npmjs.org/flat/-/flat-4.1.1.tgz" - integrity sha512-FmTtBsHskrU6FJ2VxCnsDb84wu9zhmO3cUX2kGFb5tuwhfXxGciiT0oRY+cck35QmG+NmGh5eLz6lLCpWTqwpA== - dependencies: - is-buffer "~2.0.3" +flat@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" + integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== flatted@^3.1.0: version "3.2.7" @@ -3939,11 +3956,16 @@ fs.realpath@^1.0.0: resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= -fsevents@~2.1.1, fsevents@~2.1.2: +fsevents@~2.1.2: version "2.1.3" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" 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== + fstream@^1.0.12: version "1.0.12" resolved "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz" @@ -4012,16 +4034,16 @@ gcp-metadata@^4.2.0: gaxios "^4.0.0" json-bigint "^1.0.0" -geckodriver@^1.19.1: - version "1.22.2" - resolved "https://registry.npmjs.org/geckodriver/-/geckodriver-1.22.2.tgz" - integrity sha512-xcf1OLfHqNX4+wQhj4weu2gtiwtPnV8yEEKvLkC8GuFtUc5WjOGodV/2pHiYJjCSJRQfsmIgY5Xs1zaJf/OGFA== +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.3" + adm-zip "0.5.9" bluebird "3.7.2" - got "5.6.0" - https-proxy-agent "5.0.0" - tar "6.0.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" @@ -4090,7 +4112,7 @@ glob-parent@^3.1.0: is-glob "^3.1.0" path-dirname "^1.0.0" -glob-parent@^5.1.2, glob-parent@~5.1.0: +glob-parent@^5.1.2, glob-parent@~5.1.0, glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== @@ -4137,22 +4159,10 @@ glob@*, glob@^7.0.3, glob@^7.1.0, glob@^7.1.3, glob@^7.1.4: once "^1.3.0" path-is-absolute "^1.0.0" -glob@7.1.2: - version "7.1.2" - resolved "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz" - integrity sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -glob@7.1.3: - version "7.1.3" - resolved "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz" - integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ== +glob@7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" + integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== dependencies: fs.realpath "^1.0.0" inflight "^1.0.4" @@ -4274,27 +4284,22 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" -got@5.6.0: - version "5.6.0" - resolved "https://registry.npmjs.org/got/-/got-5.6.0.tgz" - integrity sha1-ux1+4WO3gIK7yOuDbz85UATqb78= - dependencies: - create-error-class "^3.0.1" - duplexer2 "^0.1.4" - is-plain-obj "^1.0.0" - is-redirect "^1.0.0" - is-retry-allowed "^1.0.0" - is-stream "^1.0.0" - lowercase-keys "^1.0.0" - node-status-codes "^1.0.0" - object-assign "^4.0.1" - parse-json "^2.1.0" - pinkie-promise "^2.0.0" - read-all-stream "^3.0.0" - readable-stream "^2.0.5" - timed-out "^2.0.0" - unzip-response "^1.0.0" - url-parse-lax "^1.0.0" +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" @@ -4341,11 +4346,6 @@ grainjs@1.0.2, grainjs@^1.0.1: resolved "https://registry.npmjs.org/grainjs/-/grainjs-1.0.2.tgz" integrity sha512-wrj8TqpgxTGOKHpTlMBxMeX2uS3lTvXj4ROLKC+EZNM7J6RHQLGjMzMqWtiryBnMhGIBlbCicMNFppCrK1zv9w== -growl@1.10.5: - version "1.10.5" - resolved "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz" - integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== - gtoken@^5.0.4: version "5.3.0" resolved "https://registry.npmjs.org/gtoken/-/gtoken-5.3.0.tgz" @@ -4387,11 +4387,6 @@ har-validator@~5.1.3: ajv "^6.12.3" har-schema "^2.0.0" -has-bigints@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz" - integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA== - has-flag@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz" @@ -4409,7 +4404,7 @@ has-property-descriptors@^1.0.0: dependencies: get-intrinsic "^1.1.1" -has-symbols@^1.0.0, has-symbols@^1.0.1, has-symbols@^1.0.2: +has-symbols@^1.0.1, has-symbols@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz" integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== @@ -4474,11 +4469,6 @@ hdr-histogram-percentiles-obj@^3.0.0: resolved "https://registry.yarnpkg.com/hdr-histogram-percentiles-obj/-/hdr-histogram-percentiles-obj-3.0.0.tgz#9409f4de0c2dda78e61de2d9d78b1e9f3cba283c" integrity sha512-7kIufnBqdsBGcSZLPJwqHT3yhk1QTsSlFsVD3kx5ixH/AlgBs9yM1q6DPhXZ8f8gtdqgh7N7/5btRLpQsS2gHw== -he@1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/he/-/he-1.1.1.tgz" - integrity sha1-k0EP0hsAlzUVH4howvJx80J+I/0= - he@1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/he/-/he-1.2.0.tgz" @@ -4577,19 +4567,19 @@ 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" integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= -https-proxy-agent@5.0.0, https-proxy-agent@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz" - integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== - dependencies: - agent-base "6" - debug "4" - 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" @@ -4598,6 +4588,14 @@ https-proxy-agent@5.0.1: agent-base "6" debug "4" +https-proxy-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz" + integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== + dependencies: + agent-base "6" + debug "4" + humanize-ms@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" @@ -4836,16 +4834,6 @@ is-arguments@^1.0.4: call-bind "^1.0.2" has-tostringtag "^1.0.0" -is-arrayish@^0.2.1: - version "0.2.1" - resolved "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz" - integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= - -is-bigint@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.1.tgz" - integrity sha512-J0ELF4yHFxHy0cmSxZuheDOz2luOdVvqjwmEcj8H/L1JHeuEDSDbeRP+Dk9kFVk5RTFzbucJ2Kb9F7ixY2QaCg== - 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" @@ -4853,33 +4841,16 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" -is-boolean-object@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.0.tgz" - integrity sha512-a7Uprx8UtD+HWdyYwnD1+ExtTgqQtD2k/1yJgtXP6wnMm8byhkoTZRl+95LLThpzNZJ5aEvi46cdH+ayMFRwmA== - dependencies: - call-bind "^1.0.0" - is-buffer@^1.1.0, is-buffer@^1.1.5: version "1.1.6" resolved "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== -is-buffer@~2.0.3: - version "2.0.5" - resolved "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz" - integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== - is-callable@^1.1.3: version "1.2.7" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== -is-callable@^1.1.4, is-callable@^1.2.3: - version "1.2.3" - resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz" - integrity sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ== - is-ci@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz" @@ -4901,11 +4872,6 @@ is-core-module@^2.9.0: dependencies: has "^1.0.3" -is-date-object@^1.0.1: - version "1.0.2" - resolved "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz" - integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g== - 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" @@ -4967,21 +4933,11 @@ is-negated-glob@^1.0.0: resolved "https://registry.yarnpkg.com/is-negated-glob/-/is-negated-glob-1.0.0.tgz#6910bca5da8c95e784b5751b976cf5a10fee36d2" integrity sha512-czXVVn/QEmgvej1f50BZ648vUI+em0xqMq2Sn+QncCLN4zj1UAxlT+kw/6ggQTOaZPd1HqKQGEqbpQVtJucWug== -is-negative-zero@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz" - integrity sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w== - is-npm@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/is-npm/-/is-npm-4.0.0.tgz" integrity sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig== -is-number-object@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.4.tgz" - integrity sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw== - is-number@^7.0.0: version "7.0.0" resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz" @@ -5016,10 +4972,10 @@ is-path-inside@^3.0.1: resolved "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.2.tgz" integrity sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg== -is-plain-obj@^1.0.0: - version "1.1.0" - resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz" - integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4= +is-plain-obj@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" + integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== is-plain-object@^2.0.4: version "2.0.4" @@ -5033,19 +4989,6 @@ is-potential-custom-element-name@^1.0.0: resolved "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz" integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== -is-redirect@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz" - integrity sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ= - -is-regex@^1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/is-regex/-/is-regex-1.1.2.tgz" - integrity sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg== - dependencies: - call-bind "^1.0.2" - has-symbols "^1.0.1" - is-relative@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-relative/-/is-relative-1.0.0.tgz#a1bb6935ce8c5dba1e8b9754b9b2dcc020e2260d" @@ -5053,33 +4996,11 @@ is-relative@^1.0.0: dependencies: is-unc-path "^1.0.0" -is-retry-allowed@^1.0.0: - version "1.2.0" - resolved "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz" - integrity sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg== - -is-stream@^1.0.0: - version "1.1.0" - resolved "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz" - integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= - is-stream@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz" integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw== -is-string@^1.0.5: - version "1.0.5" - resolved "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz" - integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ== - -is-symbol@^1.0.2, is-symbol@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz" - integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ== - dependencies: - has-symbols "^1.0.1" - is-typed-array@^1.1.10, is-typed-array@^1.1.3: version "1.1.10" resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.10.tgz#36a5b5cb4189b575d1a3e4b08536bfb485801e3f" @@ -5103,6 +5024,11 @@ is-unc-path@^1.0.0: dependencies: unc-path-regex "^0.1.2" +is-unicode-supported@^0.1.0: + version "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" @@ -5186,14 +5112,6 @@ js-tokens@^4.0.0: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-yaml@3.13.1: - version "3.13.1" - resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz" - integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== - dependencies: - argparse "^1.0.7" - esprima "^4.0.0" - js-yaml@3.14.1: version "3.14.1" resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz" @@ -5202,9 +5120,9 @@ js-yaml@3.14.1: argparse "^1.0.7" esprima "^4.0.0" -js-yaml@^4.1.0: +js-yaml@4.1.0, js-yaml@^4.1.0: version "4.1.0" - resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== dependencies: argparse "^2.0.1" @@ -5268,6 +5186,11 @@ 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" @@ -5420,6 +5343,13 @@ 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" @@ -5515,14 +5445,6 @@ locale-currency@0.0.2: resolved "https://registry.npmjs.org/locale-currency/-/locale-currency-0.0.2.tgz" integrity sha1-4skGB1Y85HpZ+VWeRacOJOTbS20= -locate-path@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz" - integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== - dependencies: - p-locate "^3.0.0" - path-exists "^3.0.0" - locate-path@^5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz" @@ -5530,6 +5452,13 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + lodash-node@~2.4.1: version "2.4.1" resolved "https://registry.npmjs.org/lodash-node/-/lodash-node-2.4.1.tgz" @@ -5645,12 +5574,13 @@ lodash@4.17.21, lodash@^4.0.0, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19 resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== -log-symbols@3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz" - integrity sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ== +log-symbols@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== dependencies: - chalk "^2.4.2" + chalk "^4.1.0" + is-unicode-supported "^0.1.0" lolex@^3.0.0: version "3.1.0" @@ -5823,6 +5753,11 @@ mimic-response@^1.0.0, mimic-response@^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" @@ -5833,7 +5768,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@^3.0.4: +minimatch@*, minimatch@^3.0.4: version "3.0.4" resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz" integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== @@ -5848,6 +5783,13 @@ minimatch@0.3: lru-cache "2" sigmund "~1.0.0" +minimatch@5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.0.1.tgz#fb9022f7528125187c92bd9e9b6366be1cf3415b" + integrity sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g== + dependencies: + brace-expansion "^2.0.1" + minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" @@ -5855,11 +5797,6 @@ minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" -minimist@0.0.8: - version "0.0.8" - resolved "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz" - integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= - minimist@^1.1.0, minimist@^1.2.0, minimist@^1.2.5: version "1.2.5" resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz" @@ -5951,7 +5888,7 @@ minipass@^4.0.0: resolved "https://registry.yarnpkg.com/minipass/-/minipass-4.0.1.tgz#2b9408c6e81bb8b338d600fb3685e375a370a057" integrity sha512-V9esFpNbK0arbN3fm2sxDKqMYgIp7XtVdE4Esj+PE4Qaaxdg1wIw48ITQIOn1sc8xXSmUviVL3cyjMqPlrVkiA== -minizlib@^2.0.0, minizlib@^2.1.0, minizlib@^2.1.1: +minizlib@^2.0.0, minizlib@^2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== @@ -5959,14 +5896,7 @@ minizlib@^2.0.0, minizlib@^2.1.0, minizlib@^2.1.1: minipass "^3.0.0" yallist "^4.0.0" -mkdirp@0.5.1: - version "0.5.1" - resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz" - integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= - dependencies: - minimist "0.0.8" - -mkdirp@0.5.5, "mkdirp@>=0.5 0", mkdirp@^0.5.1, mkdirp@^0.5.4: +"mkdirp@>=0.5 0", mkdirp@^0.5.1, mkdirp@^0.5.4: version "0.5.5" resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz" integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== @@ -5978,66 +5908,46 @@ 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.9: - version "0.2.9" - resolved "https://registry.npmjs.org/mocha-webdriver/-/mocha-webdriver-0.2.9.tgz" - integrity sha512-WNr7Yq1uhcvLYyjDChKvW9y2hD8DnzJ0Av/LheZtiS0/6MQtPvxx1XO7OtdnxiS6my9gKg4kRe0944dggLGEFg== +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== dependencies: chai "^4.1.2" chai-as-promised "^7.1.1" chromedriver "^74.0.0" fs-extra "^8.0.1" - geckodriver "^1.19.1" - mocha "^7.1.2" + geckodriver "^3.2.0" + mocha "^10.1.0" npm-run-path "^3.1.0" selenium-webdriver "^4.0.0-alpha.1" -mocha@5.2.0: - version "5.2.0" - resolved "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz" - integrity sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ== - dependencies: - browser-stdout "1.3.1" - commander "2.15.1" - debug "3.1.0" - diff "3.5.0" - escape-string-regexp "1.0.5" - glob "7.1.2" - growl "1.10.5" - he "1.1.1" - minimatch "3.0.4" - mkdirp "0.5.1" - supports-color "5.4.0" - -mocha@^7.1.2: - version "7.2.0" - resolved "https://registry.npmjs.org/mocha/-/mocha-7.2.0.tgz" - integrity sha512-O9CIypScywTVpNaRrCAgoUnJgozpIofjKUYmJhiCIJMiuYnLI6otcb1/kpW9/n/tJODHGZ7i8aLQoDVsMtOKQQ== +mocha@10.2.0, mocha@^10.1.0: + version "10.2.0" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.2.0.tgz#1fd4a7c32ba5ac372e03a17eef435bd00e5c68b8" + integrity sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg== dependencies: - ansi-colors "3.2.3" + ansi-colors "4.1.1" browser-stdout "1.3.1" - chokidar "3.3.0" - debug "3.2.6" - diff "3.5.0" - escape-string-regexp "1.0.5" - find-up "3.0.0" - glob "7.1.3" - growl "1.10.5" + chokidar "3.5.3" + debug "4.3.4" + diff "5.0.0" + escape-string-regexp "4.0.0" + find-up "5.0.0" + glob "7.2.0" he "1.2.0" - js-yaml "3.13.1" - log-symbols "3.0.0" - minimatch "3.0.4" - mkdirp "0.5.5" - ms "2.1.1" - node-environment-flags "1.0.6" - object.assign "4.1.0" - strip-json-comments "2.0.1" - supports-color "6.0.0" - which "1.3.1" - wide-align "1.1.3" - yargs "13.3.2" - yargs-parser "13.1.2" - yargs-unparser "1.6.0" + js-yaml "4.1.0" + log-symbols "4.1.0" + minimatch "5.0.1" + ms "2.1.3" + nanoid "3.3.3" + serialize-javascript "6.0.0" + strip-json-comments "3.1.1" + supports-color "8.1.1" + workerpool "6.2.1" + yargs "16.2.0" + yargs-parser "20.2.4" + yargs-unparser "2.0.0" module-deps@^4.0.8: version "4.1.1" @@ -6105,17 +6015,12 @@ ms@2.0.0: resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= -ms@2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz" - integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== - ms@2.1.2, ms@^2.1.1: version "2.1.2" resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@^2.0.0: +ms@2.1.3, ms@^2.0.0: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -6138,6 +6043,11 @@ mz@^2.4.0: object-assign "^4.0.1" thenify-all "^1.0.0" +nanoid@3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.3.tgz#fd8e8b7aa761fe807dba2d1b98fb7241bb724a25" + integrity sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w== + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" @@ -6192,14 +6102,6 @@ node-addon-api@^4.2.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-4.3.0.tgz#52a1a0b475193e0928e98e0426a0d1254782b77f" integrity sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ== -node-environment-flags@1.0.6: - version "1.0.6" - resolved "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.6.tgz" - integrity sha512-5Evy2epuL+6TM0lCQGpFIj6KwiEsGh1SrHUhTbNX+sLbBtjidPZFAnVK9y5yU1+h//RitLbRHTIMyxQPtxMdHw== - dependencies: - object.getownpropertydescriptors "^2.0.3" - semver "^5.7.0" - node-fetch@2.6.7: version "2.6.7" resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz" @@ -6260,11 +6162,6 @@ node-releases@^2.0.6: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.8.tgz#0f349cdc8fcfa39a92ac0be9bc48b7706292b9ae" integrity sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A== -node-status-codes@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/node-status-codes/-/node-status-codes-1.0.0.tgz" - integrity sha1-WuVUHQJGRdMqWPzdyc7s6nrjrC8= - nodemon@^2.0.4: version "2.0.4" resolved "https://registry.npmjs.org/nodemon/-/nodemon-2.0.4.tgz" @@ -6312,6 +6209,11 @@ 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" @@ -6366,21 +6268,11 @@ object-inspect@^1.9.0: resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz" integrity sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw== -object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1: +object-keys@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== -object.assign@4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz" - integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w== - dependencies: - define-properties "^1.1.2" - function-bind "^1.1.1" - has-symbols "^1.0.0" - object-keys "^1.0.11" - object.assign@^4.0.4: version "4.1.4" resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.4.tgz#9673c7c7c351ab8c4d0b516f4343ebf4dfb7799f" @@ -6391,25 +6283,6 @@ object.assign@^4.0.4: has-symbols "^1.0.3" object-keys "^1.1.1" -object.assign@^4.1.2: - version "4.1.2" - resolved "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz" - integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ== - dependencies: - call-bind "^1.0.0" - define-properties "^1.1.3" - has-symbols "^1.0.1" - object-keys "^1.1.1" - -object.getownpropertydescriptors@^2.0.3: - version "2.1.2" - resolved "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.2.tgz" - integrity sha512-WtxeKSzfBjlzL+F9b7M7hewDzMwy+C8NRssHd1YrNlzHzIDrXcXiNOMrezdAEM4UXixgV+vvnyBeN7Rygl2ttQ== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.18.0-next.2" - on-finished@~2.3.0: version "2.3.0" resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz" @@ -6484,19 +6357,24 @@ p-cancelable@^1.0.0: resolved "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz" integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw== -p-limit@^2.0.0, p-limit@^2.2.0: +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" integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== dependencies: p-try "^2.0.0" -p-locate@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz" - integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== dependencies: - p-limit "^2.0.0" + yocto-queue "^0.1.0" p-locate@^4.1.0: version "4.1.0" @@ -6505,6 +6383,13 @@ p-locate@^4.1.0: dependencies: p-limit "^2.2.0" +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + 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" @@ -6568,13 +6453,6 @@ parse-asn1@^5.0.0, parse-asn1@^5.1.5: pbkdf2 "^3.0.3" safe-buffer "^5.1.1" -parse-json@^2.1.0: - version "2.2.0" - resolved "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz" - integrity sha1-9ID0BDTvgHQfhGkJn43qGPVaTck= - dependencies: - error-ex "^1.2.0" - parse5-htmlparser2-tree-adapter@^6.0.0: version "6.0.1" resolved "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz" @@ -6614,11 +6492,6 @@ path-dirname@^1.0.0: resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" integrity sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q== -path-exists@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz" - integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= - path-exists@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz" @@ -6864,11 +6737,6 @@ prelude-ls@~1.1.2: resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz" integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w== -prepend-http@^1.0.1: - version "1.0.4" - resolved "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz" - integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw= - prepend-http@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz" @@ -7046,6 +6914,11 @@ 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" @@ -7096,14 +6969,6 @@ rc@^1.2.8: minimist "^1.2.0" strip-json-comments "~2.0.1" -read-all-stream@^3.0.0: - version "3.1.0" - resolved "https://registry.npmjs.org/read-all-stream/-/read-all-stream-3.1.0.tgz" - integrity sha1-NcPhd/IHjveJ7kv6+kNzB06u9Po= - dependencies: - pinkie-promise "^2.0.0" - readable-stream "^2.0.0" - read-only-stream@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/read-only-stream/-/read-only-stream-2.0.0.tgz" @@ -7159,13 +7024,6 @@ readdir-glob@^1.0.0: dependencies: minimatch "^3.0.4" -readdirp@~3.2.0: - version "3.2.0" - resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.2.0.tgz" - integrity sha512-crk4Qu3pmXwgxdSgGhgA/eXiJAPQiX4GMOZZMXnqKxHX7TaoL+3gQVo/WeuAiogr07DpnfjIMpXXa+PAIvwPGQ== - dependencies: - picomatch "^2.0.4" - readdirp@~3.4.0: version "3.4.0" resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz" @@ -7173,6 +7031,13 @@ readdirp@~3.4.0: dependencies: picomatch "^2.2.1" +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + rechoir@^0.7.0: version "0.7.1" resolved "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz" @@ -7346,6 +7211,11 @@ 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" @@ -7404,6 +7274,13 @@ 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" @@ -7515,7 +7392,7 @@ semver-diff@^3.1.1: dependencies: semver "^6.3.0" -semver@^5.7.0, semver@^5.7.1: +semver@^5.7.1: version "5.7.1" resolved "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== @@ -7551,9 +7428,9 @@ send@0.16.2: range-parser "~1.2.0" statuses "~1.4.0" -serialize-javascript@^6.0.0: +serialize-javascript@6.0.0, serialize-javascript@^6.0.0: version "6.0.0" - resolved "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8" integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag== dependencies: randombytes "^2.1.0" @@ -7884,14 +7761,6 @@ 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": - version "2.1.1" - resolved "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz" - integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== - dependencies: - is-fullwidth-code-point "^2.0.0" - strip-ansi "^4.0.0" - "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.2.3: version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" @@ -7901,7 +7770,7 @@ strict-uri-encode@^2.0.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" -string-width@^3.0.0, string-width@^3.1.0: +string-width@^3.0.0: version "3.1.0" resolved "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz" integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== @@ -7928,22 +7797,6 @@ string-width@^4.2.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.0" -string.prototype.trimend@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz" - integrity sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - -string.prototype.trimstart@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz" - integrity sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - string_decoder@^1.1.1: version "1.3.0" resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" @@ -7970,14 +7823,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -strip-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz" - integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= - dependencies: - ansi-regex "^3.0.0" - -strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: +strip-ansi@^5.1.0: version "5.2.0" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz" integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== @@ -7998,16 +7844,16 @@ strip-ansi@^6.0.1: dependencies: ansi-regex "^5.0.1" -strip-json-comments@2.0.1, strip-json-comments@~2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz" - integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= - -strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: +strip-json-comments@3.1.1, strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz" + integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= + strnum@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/strnum/-/strnum-1.0.5.tgz#5c4e829fe15ad4ff0d20c3db5ac97b73c9b072db" @@ -8028,19 +7874,12 @@ subarg@^1.0.0: dependencies: minimist "^1.1.0" -supports-color@5.4.0: - version "5.4.0" - resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz" - integrity sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w== - dependencies: - has-flag "^3.0.0" - -supports-color@6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz" - integrity sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg== +supports-color@8.1.1, supports-color@^8.0.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== dependencies: - has-flag "^3.0.0" + has-flag "^4.0.0" supports-color@^5.3.0, supports-color@^5.5.0: version "5.5.0" @@ -8056,13 +7895,6 @@ supports-color@^7.1.0: dependencies: has-flag "^4.0.0" -supports-color@^8.0.0: - version "8.1.1" - resolved "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz" - integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== - dependencies: - has-flag "^4.0.0" - supports-preserve-symlinks-flag@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz" @@ -8096,15 +7928,15 @@ tar-stream@^2.2.0: inherits "^2.0.3" readable-stream "^3.1.1" -tar@6.0.2: - version "6.0.2" - resolved "https://registry.npmjs.org/tar/-/tar-6.0.2.tgz" - integrity sha512-Glo3jkRtPcvpDlAs/0+hozav78yoXKFr+c4wgw62NNMO3oo4AaJdCo21Uu7lcwr55h39W2XD1LMERc64wtbItg== +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.0" + minizlib "^2.1.1" mkdirp "^1.0.3" yallist "^4.0.0" @@ -8214,11 +8046,6 @@ through2@^4.0.0: resolved "https://registry.npmjs.org/through/-/through-2.3.8.tgz" integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= -timed-out@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/timed-out/-/timed-out-2.0.0.tgz" - integrity sha1-84sK6B03R9YoAB9B2vxlKs5nHAo= - timers-browserify@^1.0.1: version "1.4.2" resolved "https://registry.npmjs.org/timers-browserify/-/timers-browserify-1.4.2.tgz" @@ -8498,16 +8325,6 @@ umd@^3.0.0: resolved "https://registry.npmjs.org/umd/-/umd-3.0.3.tgz" integrity sha512-4IcGSufhFshvLNcMCV80UnQVlZ5pMOC8mvNPForqwA4+lzYQuetTESLDQkeLmihq8bRcnpbQa48Wb8Lh16/xow== -unbox-primitive@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz" - integrity sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw== - dependencies: - function-bind "^1.1.1" - has-bigints "^1.0.1" - has-symbols "^1.0.2" - which-boxed-primitive "^1.0.2" - unc-path-regex@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" @@ -8580,11 +8397,6 @@ unpipe@1.0.0, unpipe@~1.0.0: resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= -unzip-response@^1.0.0: - version "1.0.2" - resolved "https://registry.npmjs.org/unzip-response/-/unzip-response-1.0.2.tgz" - integrity sha1-uYTwh3/AqJwsdzzB73tbIytbBv4= - unzipper@^0.10.11: version "0.10.11" resolved "https://registry.npmjs.org/unzipper/-/unzipper-0.10.11.tgz" @@ -8643,13 +8455,6 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" -url-parse-lax@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz" - integrity sha1-evjzA2Rem9eaJy56FKxovAYJ2nM= - dependencies: - prepend-http "^1.0.1" - url-parse-lax@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz" @@ -8951,17 +8756,6 @@ whatwg-url@^8.0.0: tr46 "^2.1.0" webidl-conversions "^6.1.0" -which-boxed-primitive@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz" - integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== - dependencies: - is-bigint "^1.0.1" - is-boolean-object "^1.1.0" - is-number-object "^1.0.4" - is-string "^1.0.5" - is-symbol "^1.0.3" - which-module@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz" @@ -8979,13 +8773,6 @@ which-typed-array@^1.1.2: has-tostringtag "^1.0.0" is-typed-array "^1.1.10" -which@1.3.1: - version "1.3.1" - resolved "https://registry.npmjs.org/which/-/which-1.3.1.tgz" - integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== - dependencies: - isexe "^2.0.0" - which@^2.0.1, which@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" @@ -9000,13 +8787,6 @@ why-is-node-running@2.0.3: dependencies: stackback "0.0.2" -wide-align@1.1.3: - version "1.1.3" - resolved "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz" - integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== - dependencies: - string-width "^1.0.2 || 2" - wide-align@^1.1.2, wide-align@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3" @@ -9048,14 +8828,10 @@ wordwrap@^1.0.0: resolved "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz" integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== -wrap-ansi@^5.1.0: - version "5.1.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz" - integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q== - dependencies: - ansi-styles "^3.2.0" - string-width "^3.0.0" - strip-ansi "^5.0.0" +workerpool@6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" + integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== wrap-ansi@^6.2.0: version "6.2.0" @@ -9211,13 +8987,10 @@ yallist@^4.0.0: resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== -yargs-parser@13.1.2, yargs-parser@^13.1.2: - version "13.1.2" - resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz" - integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" +yargs-parser@20.2.4: + version "20.2.4" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" + integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== yargs-parser@^18.1.2: version "18.1.3" @@ -9237,30 +9010,28 @@ yargs-parser@^21.0.0: resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz" integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== -yargs-unparser@1.6.0: - version "1.6.0" - resolved "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.6.0.tgz" - integrity sha512-W9tKgmSn0DpSatfri0nx52Joq5hVXgeLiqR/5G0sZNDoLZFOr/xjBUDcShCOGNsBnEMNo1KAMBkTej1Hm62HTw== +yargs-unparser@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb" + integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA== dependencies: - flat "^4.1.0" - lodash "^4.17.15" - yargs "^13.3.0" + camelcase "^6.0.0" + decamelize "^4.0.0" + flat "^5.0.2" + is-plain-obj "^2.1.0" -yargs@13.3.2, yargs@^13.3.0: - version "13.3.2" - resolved "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz" - integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw== +yargs@16.2.0, yargs@^16.0.0: + version "16.2.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== dependencies: - cliui "^5.0.0" - find-up "^3.0.0" - get-caller-file "^2.0.1" + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" require-directory "^2.1.1" - require-main-filename "^2.0.0" - set-blocking "^2.0.0" - string-width "^3.0.0" - which-module "^2.0.0" - y18n "^4.0.0" - yargs-parser "^13.1.2" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" yargs@^15.3.1: version "15.4.1" @@ -9279,19 +9050,6 @@ yargs@^15.3.1: y18n "^4.0.0" yargs-parser "^18.1.2" -yargs@^16.0.0: - version "16.2.0" - resolved "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz" - integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== - dependencies: - cliui "^7.0.2" - escalade "^3.1.1" - get-caller-file "^2.0.5" - require-directory "^2.1.1" - string-width "^4.2.0" - y18n "^5.0.5" - yargs-parser "^20.2.2" - yargs@^17.3.1: version "17.5.1" resolved "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz" @@ -9313,6 +9071,11 @@ yauzl@^2.10.0: 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" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + zip-stream@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.0.tgz"