diff --git a/ci/Jenkinsfiles/build.groovy b/ci/Jenkinsfiles/build.groovy
index a273d495..b959ab73 100644
--- a/ci/Jenkinsfiles/build.groovy
+++ b/ci/Jenkinsfiles/build.groovy
@@ -108,6 +108,17 @@ pipeline {
steps {
script {
def stages = [:]
+ stages['Frontend'] = {
+ container('playwright') {
+ nxWithGitHubStatus(context: 'utests/frontend') {
+ dir('nuxeo-retention-web') {
+ sh 'npm install --no-save playwright'
+ sh 'npx playwright install --with-deps'
+ sh 'npm run test'
+ }
+ }
+ }
+ }
stages['Backend - dev'] = {
container('maven') {
nxWithGitHubStatus(context: 'utests/backend/dev') {
diff --git a/nuxeo-retention-web/karma.conf.js b/nuxeo-retention-web/karma.conf.js
deleted file mode 100644
index 6e1c6ef1..00000000
--- a/nuxeo-retention-web/karma.conf.js
+++ /dev/null
@@ -1,125 +0,0 @@
-const path = require('path');
-
-const coverage = process.argv.find((arg) => arg.includes('coverage'));
-
-const reporters = coverage ? ['mocha', 'coverage-istanbul'] : ['mocha'];
-
-let customLaunchers = {
- ChromeHeadlessNoSandbox: {
- base: 'ChromeHeadless',
- flags: ['--disable-gpu', '--no-sandbox'],
- },
-};
-
-if (process.env.SAUCE_USERNAME && process.env.SAUCE_ACCESS_KEY) {
- customLaunchers = {
- sl_latest_chrome: {
- base: 'SauceLabs',
- browserName: 'chrome',
- platform: 'Windows 10',
- version: 'latest',
- },
- sl_latest_edge: {
- base: 'SauceLabs',
- browserName: 'microsoftedge',
- platform: 'Windows 10',
- version: 'latest',
- },
- sl_latest_safari: {
- base: 'SauceLabs',
- browserName: 'safari',
- platform: 'macOS 12',
- version: 'latest',
- },
- };
-
- reporters.push('saucelabs');
-}
-
-module.exports = (config) => {
- const sauceLabs = {};
- if (config.record) {
- sauceLabs.recordVideo = true;
- } else if (config.sauceRunName) {
- sauceLabs.testName = config.sauceRunName;
- }
-
- config.set({
- sauceLabs,
- basePath: '',
- singleRun: true,
- browsers: config.browsers && config.browsers.length > 0 ? config.browsers : Object.keys(customLaunchers),
- browserDisconnectTimeout: 10 * 1000,
- browserDisconnectTolerance: 1,
- browserNoActivityTimeout: 5 * 60 * 1000,
- customLaunchers,
- middleware: ['static'],
- static: {
- path: path.join(process.cwd(), ''),
- },
- files: [
- {
- pattern: `test/*${config.grep || '*.test.js'}`,
- type: 'module',
- },
- ],
- plugins: [
- // load plugin
- require.resolve('@open-wc/karma-esm'),
-
- // fallback: resolve any karma- plugins
- 'karma-*',
- ],
- frameworks: ['esm', 'mocha', 'sinon-chai', 'source-map-support'],
- esm: {
- // prevent auto loading of polyfills
- compatibility: 'none',
- coverage,
- // if you are using 'bare module imports' you will need this option
- nodeResolve: true,
- // needed for npm link or lerna support
- preserveSymlinks: true,
- },
-
- reporters,
- port: 9876,
- colors: true,
- browserConsoleLogOptions: {
- level: 'error',
- },
- logLevel: config.LOG_WARN,
- /** Some errors come in JSON format with a message property. */
- formatError(error) {
- try {
- if (typeof error !== 'string') {
- return error;
- }
- const parsed = JSON.parse(error);
- if (typeof parsed !== 'object' || !parsed.message) {
- return error;
- }
- return parsed.message;
- } catch (_) {
- return error;
- }
- },
-
- coverageIstanbulReporter: {
- reports: ['html', 'lcovonly', 'text-summary'],
- dir: path.join(__dirname, 'coverage'),
- combineBrowserReports: true,
- skipFilesWithNoCoverage: true,
- },
-
- client: {
- mocha: {
- reporter: 'html',
- ui: 'tdd',
- timeout: 3000,
- },
- chai: {
- includeStack: true,
- },
- },
- });
-};
diff --git a/nuxeo-retention-web/package.json b/nuxeo-retention-web/package.json
index a9818421..0d8d91d7 100644
--- a/nuxeo-retention-web/package.json
+++ b/nuxeo-retention-web/package.json
@@ -6,27 +6,22 @@
"license": "Apache-2.0",
"devDependencies": {
"@cucumber/cucumber": "^7.0.0",
+ "@esm-bundle/chai": "^4.3.4",
+ "@nuxeo/moment": "^2.24.0-nx.0",
"@nuxeo/nuxeo-web-ui-ftest": "3.0.27-rc.0",
"@nuxeo/testing-helpers": "^3.1.5",
- "@open-wc/karma-esm": "^2.16.18",
"@rollup/plugin-node-resolve": "^7.1.3",
- "chai": "^5.0.3",
+ "@web/dev-server-legacy": "^0.1.7",
+ "@web/test-runner": "^0.13.31",
+ "@web/test-runner-playwright": "^0.8.10",
+ "@web/test-runner-saucelabs": "^0.5.0",
+ "chai": "^5.1.0",
"eslint": "^7.12.1",
"eslint-config-airbnb-base": "^14.2.0",
"eslint-config-prettier": "^6.15.0",
"eslint-plugin-html": "^6.1.0",
"eslint-plugin-import": "^2.22.1",
"husky": "^4.3.0",
- "karma": "^6.4.2",
- "karma-chrome-launcher": "^3.2.0",
- "karma-coverage-istanbul-reporter": "^3.0.3",
- "karma-firefox-launcher": "^2.1.2",
- "karma-mocha": "^2.0.1",
- "karma-mocha-reporter": "^2.2.5",
- "karma-sauce-launcher": "^4.3.6",
- "karma-sinon-chai": "^2.0.2",
- "karma-source-map-support": "^1.4.0",
- "karma-static": "^1.0.1",
"lint-staged": "^10.5.1",
"polymer-cli": "^1.9.11",
"prettier": "2.1.2",
@@ -34,6 +29,7 @@
"rollup-plugin-copy": "^3.3.0",
"rollup-plugin-minify-html-literals": "^1.2.5",
"rollup-plugin-terser": "^7.0.2",
+ "sinon": "^17.0.1",
"sinon-chai": "^3.7.0"
},
"dependencies": {
@@ -67,7 +63,7 @@
"format:prettier": "prettier \"**/*.{js,html}\" --write",
"ftest": "cd ftest && nuxeo-web-ui-ftest --screenshots --report --headless",
"ftest:watch": "cd ftest && nuxeo-web-ui-ftest --debug --tags=@watch",
- "test": "karma start --coverage",
- "test:watch": "karma start --auto-watch=true --single-run=false"
+ "test": "web-test-runner",
+ "test:watch": "web-test-runner --watch"
}
}
diff --git a/nuxeo-retention-web/test/nuxeo-attach-rule-button.test.js b/nuxeo-retention-web/test/nuxeo-attach-rule-button.test.js
new file mode 100644
index 00000000..ad208ae3
--- /dev/null
+++ b/nuxeo-retention-web/test/nuxeo-attach-rule-button.test.js
@@ -0,0 +1,329 @@
+/**
+@license
+©2023 Hyland Software, Inc. and its affiliates. All rights reserved.
+All Hyland product names are registered or unregistered trademarks of Hyland Software, Inc. or its affiliates.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use attachEl file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+import { fixture, html } from '@nuxeo/testing-helpers';
+import '../elements/nuxeo-attach-rule-button.js';
+import sinon from 'sinon';
+import { expect } from 'chai';
+
+const document = {
+ 'entity-type': 'document',
+ contextParameters: {
+ attachEl: {
+ entries: [
+ {
+ path: '/default-domain',
+ title: 'Domain',
+ type: 'Domain',
+ uid: '1',
+ },
+ {
+ path: '/default-domain/workspaces',
+ title: 'Workspaces',
+ type: 'WorkspaceRoot',
+ uid: '2',
+ },
+ {
+ path: '/default-domain/workspaces/my workspace',
+ title: 'my workspace',
+ type: 'Workspace',
+ uid: '3',
+ },
+ {
+ path: '/default-domain/workspaces/my workspace/folder 1',
+ title: 'folder 1',
+ type: 'Folder',
+ uid: '4',
+ },
+ {
+ path: '/default-domain/workspaces/my workspace/folder 1/folder 2',
+ title: 'folder 2',
+ type: 'Folder',
+ uid: '5',
+ },
+ {
+ path: '/default-domain/workspaces/my workspace/folder 1/folder 2/folder 3',
+ title: 'folder 3',
+ type: 'Folder',
+ uid: '6',
+ },
+ {
+ path: '/default-domain/workspaces/my workspace/folder 1/folder 2/folder 3/my file',
+ title: 'my file',
+ type: 'File',
+ uid: '7',
+ },
+ ],
+ },
+ },
+ path: '/default-domain/workspaces/my workspace/folder 1/folder 2/folder 3/my file',
+ title: 'my file',
+ type: 'File',
+ uid: '7',
+};
+
+window.nuxeo.I18n.language = 'en';
+window.nuxeo.I18n.en = window.nuxeo.I18n.en || {};
+window.nuxeo.I18n.en['retention.rule.attachButton.label.heading'] = 'Attach retention rule';
+window.nuxeo.I18n.en['retention.rule.attachButton.attached'] = 'Retention rule attached';
+
+suite('nuxeo-attach-rule-button', () => {
+ let attachEl;
+
+ setup(async () => {
+ attachEl = await fixture(html` `);
+ });
+
+ suite('test _isAvailable', () => {
+ test('Should return provider object if provider is available', async () => {
+ const providerObj = {
+ provider: true,
+ };
+ attachEl.provider = providerObj;
+ sinon.stub(attachEl, 'canSetRetention').returns(true);
+ expect(attachEl._isAvailable()).equal(providerObj);
+ });
+
+ test('Should return true if provider is not available and setRetention permission is present', async () => {
+ attachEl.provider = null;
+ sinon.stub(attachEl, 'canSetRetention').returns(true);
+ expect(attachEl._isAvailable()).equal(true);
+ });
+
+ test('Should return false if provider is not available and setRetention permission is not present', async () => {
+ attachEl.provider = null;
+ sinon.stub(attachEl, 'canSetRetention').returns(false);
+ expect(attachEl._isAvailable()).equal(false);
+ });
+ });
+
+ suite('test _computeLabel', () => {
+ test('Should compute and return label', async () => {
+ expect(attachEl._computeLabel()).equal('Attach retention rule');
+ });
+ });
+
+ suite('test _toggleDialog', () => {
+ test('Should toggle dialog', async () => {
+ sinon.spy(attachEl, 'set');
+ const dialogSpy = sinon.spy(attachEl.$.dialog, 'toggle');
+ attachEl._toggleDialog();
+ expect(attachEl.set.calledWith('rule', undefined)).to.equal(true);
+ expect(dialogSpy.calledOnce).to.equal(true);
+ });
+ });
+
+ suite('test _isValid', () => {
+ test('Should return the rule object if rule is present and document is valid', async () => {
+ const ruleObj = {
+ rule: 'some rule',
+ };
+ attachEl.document = document;
+ attachEl.rule = ruleObj;
+ expect(attachEl._isValid()).to.equal(ruleObj);
+ });
+
+ test('Should return the rule object if rule is present and provider object is present', async () => {
+ const providerObj = {
+ provider: true,
+ };
+ attachEl.provider = providerObj;
+ const ruleObj = {
+ rule: 'some rule',
+ };
+ attachEl.document = document;
+ attachEl.rule = ruleObj;
+ expect(attachEl._isValid()).to.equal(ruleObj);
+ });
+
+ test('Should return null if rule is valid but neither provider object nor document is present', async () => {
+ const ruleObj = {
+ rule: 'some rule',
+ };
+ attachEl.rule = ruleObj;
+ attachEl.provider = null;
+ attachEl.document = null;
+ expect(attachEl._isValid()).to.equal(null);
+ });
+
+ test('Should return document object if rule and provider are not present', async () => {
+ attachEl.document = document;
+ attachEl.rule = null;
+ attachEl.provider = null;
+ expect(attachEl._isValid()).to.equal(null);
+ });
+
+ test('Should return provider object if rule and document are not present', async () => {
+ const providerObj = {
+ provider: true,
+ };
+ attachEl.provider = providerObj;
+ attachEl.document = null;
+ attachEl.rule = null;
+ expect(attachEl._isValid()).to.equal(null);
+ });
+ });
+
+ suite('test _filterRules', () => {
+ test('Should return true if provider is present', async () => {
+ const providerObj = {
+ provider: true,
+ };
+ const rule = {};
+ attachEl.provider = providerObj;
+ expect(attachEl._filterRules(rule)).equals(true);
+ });
+
+ test('Should return true if retention rule doc types match the current document doc type', async () => {
+ const rule = {
+ properties: {
+ 'retention_rule:docTypes': ['file'],
+ },
+ };
+ attachEl.document.type = 'file';
+ expect(attachEl._filterRules(rule)).equals(true);
+ });
+
+ test('Should return true if there are no retention rule doc types to match', async () => {
+ const rule = {
+ properties: {
+ 'retention_rule:docTypes': ['file'],
+ },
+ };
+ attachEl.document.type = 'file';
+ expect(attachEl._filterRules(rule)).equals(true);
+ });
+ });
+
+ suite('test _ruleResultFormatter', () => {
+ test('Should format the result if doc description property exists', async () => {
+ const doc = {
+ title: 'title1',
+ properties: {
+ 'dc:description': 'description',
+ },
+ };
+ sinon.stub(attachEl, '_escapeHTML').returns('title1');
+ expect(attachEl._ruleResultFormatter(doc)).equal(
+ 'title1title1',
+ );
+ });
+
+ test('Should return the result without formatting if doc description property does not exists', async () => {
+ const doc = {
+ title: 'title1',
+ };
+ sinon.stub(attachEl, '_escapeHTML').returns('title1');
+ expect(attachEl._ruleResultFormatter(doc)).equal('title1');
+ });
+ });
+
+ suite('test _ruleSelectionFormatter', () => {
+ test('Should format the result if doc description property exists', async () => {
+ const doc = {
+ title: 'title1',
+ };
+ sinon.stub(attachEl, '_escapeHTML').returns('title1');
+ expect(attachEl._ruleResultFormatter(doc)).equal('title1');
+ });
+ });
+
+ suite('test _escapeHTML', () => {
+ test('Should return markup as it is if it is not a string', async () => {
+ const markup = 3;
+ expect(attachEl._escapeHTML(markup)).equal(3);
+ });
+
+ test('Should escape certain HTML entities and return formatted markup if it is a string', async () => {
+ const markup = 'abc>xyz {
+ test('Should dispatch notify event', async () => {
+ sinon.spy(attachEl, 'dispatchEvent');
+ attachEl._onPollStart();
+ expect(
+ attachEl.dispatchEvent.calledWith(
+ new CustomEvent('notify', {
+ composed: true,
+ bubbles: true,
+ detail: { message: 'Attaching retention rule' },
+ }),
+ ),
+ ).to.equal(true);
+ });
+ });
+
+ suite('test _onResponse', () => {
+ test('Should dispatch notify and refresh events once has executed', async () => {
+ sinon.stub(attachEl.$.waitEs, 'execute').resolves();
+ sinon.spy(attachEl, 'dispatchEvent');
+ attachEl._onResponse();
+ setTimeout(() => {
+ expect(attachEl.dispatchEvent.calledTwice).to.equal(true);
+ }, 0);
+ });
+ });
+
+ suite('test _attach', () => {
+ test('Should execute Bulk.RunAction api call if provider is present', async () => {
+ const providerObj = {
+ provider: true,
+ };
+ const rule = {
+ uid: '1',
+ };
+ attachEl.rule = rule;
+ attachEl.provider = providerObj;
+ sinon.spy(attachEl, '_toggleDialog');
+ sinon.stub(attachEl.$.attachRuleOp, 'execute').resolves();
+ sinon.spy(attachEl, 'dispatchEvent');
+ attachEl._attach();
+ expect(attachEl.$.attachRuleOp.op).equal('Bulk.RunAction');
+ expect(attachEl.$.attachRuleOp.input).equal(providerObj);
+ expect(attachEl.$.attachRuleOp.async).equal(true);
+ expect(attachEl.$.attachRuleOp.params).to.deep.equal({
+ action: 'attachRetentionRule',
+ parameters: '{"ruleId":"1"}',
+ });
+ setTimeout(() => {
+ expect(attachEl._toggleDialog.calledOnce).to.equal(true);
+ }, 0);
+ });
+ test('Should execute Retention.AttachRule api call if provider is not present', async () => {
+ const rule = {
+ uid: '1',
+ };
+ attachEl.rule = rule;
+ attachEl.provider = null;
+ sinon.spy(attachEl, '_toggleDialog');
+ sinon.stub(attachEl.$.attachRuleOp, 'execute').resolves();
+ sinon.spy(attachEl, 'dispatchEvent');
+ attachEl._attach();
+ expect(attachEl.$.attachRuleOp.op).equal('Retention.AttachRule');
+ expect(attachEl.$.attachRuleOp.input).equal(attachEl.document);
+ expect(attachEl.$.attachRuleOp.async).equal(false);
+ expect(attachEl.$.attachRuleOp.params).to.deep.equal({ rule: '1' });
+ setTimeout(() => {
+ expect(attachEl._toggleDialog.calledOnce).to.equal(true);
+ expect(attachEl.dispatchEvent.calledOnce).to.equal(true);
+ }, 0);
+ });
+ });
+});
diff --git a/nuxeo-retention-web/web-test-runner.config.js b/nuxeo-retention-web/web-test-runner.config.js
new file mode 100644
index 00000000..76919b7f
--- /dev/null
+++ b/nuxeo-retention-web/web-test-runner.config.js
@@ -0,0 +1,70 @@
+/* eslint import/no-extraneous-dependencies: ["error", {"devDependencies": true}] */
+const { createSauceLabsLauncher } = require('@web/test-runner-saucelabs');
+const { legacyPlugin } = require('@web/dev-server-legacy');
+const { playwrightLauncher } = require('@web/test-runner-playwright');
+
+const baseConfig = {
+ files: 'test/**/*.test.js',
+ browsers: [
+ playwrightLauncher({ product: 'chromium' }),
+ playwrightLauncher({ product: 'webkit' }),
+ playwrightLauncher({ product: 'firefox' }),
+ ],
+ coverage: true,
+ nodeResolve: true,
+ testFramework: {
+ config: {
+ ui: 'tdd',
+ },
+ },
+};
+
+const isSauceLabsRun = process.env.SAUCE_USERNAME && process.env.SAUCE_ACCESS_KEY;
+if (isSauceLabsRun) {
+ const sharedCapabilities = {
+ 'sauce:options': {
+ name: 'Nuxeo Cold Storage',
+ build: `Nuxeo Cold Storage ${process.env.BRANCH_NAME || 'local'} build ${process.env.BUILD_NUMBER || ''}`,
+ },
+ };
+
+ const sauceLabsLauncher = createSauceLabsLauncher({
+ user: process.env.SAUCE_USERNAME,
+ key: process.env.SAUCE_ACCESS_KEY,
+ });
+
+ const sauceBrowsers = [
+ sauceLabsLauncher({
+ ...sharedCapabilities,
+ browserName: 'chrome',
+ browserVersion: 'latest',
+ platformName: 'Windows 10',
+ }),
+ sauceLabsLauncher({
+ ...sharedCapabilities,
+ browserName: 'firefox',
+ browserVersion: 'latest',
+ platformName: 'Windows 10',
+ }),
+ sauceLabsLauncher({
+ ...sharedCapabilities,
+ browserName: 'safari',
+ browserVersion: 'latest',
+ platformName: 'macOS 10.15',
+ }),
+ sauceLabsLauncher({
+ ...sharedCapabilities,
+ browserName: 'MicrosoftEdge',
+ browserVersion: 'latest',
+ platformName: 'Windows 10',
+ }),
+ ];
+ baseConfig.browsers = baseConfig.browsers.concat(sauceBrowsers);
+ baseConfig.browserStartTimeout = 1000 * 30 * 5;
+ baseConfig.sessionStartTimeout = 1000 * 30 * 5;
+ baseConfig.sessionFinishTimeout = 1000 * 30 * 5;
+ baseConfig.plugins = [legacyPlugin()];
+ baseConfig.testFramework.config.timeout = '10000';
+}
+
+module.exports = baseConfig;