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;