diff --git a/package-lock.json b/package-lock.json index 0881b5b7..929bcedd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@75lb/deep-merge": "^1.1.2", "@rollup/plugin-replace": "5.0.5", + "@testing-library/user-event": "^14.5.2", "@web/dev-server-rollup": "0.6.1", "@web/test-runner": "0.18.1", "@web/test-runner-commands": "0.9.0", @@ -1927,7 +1928,6 @@ "version": "7.23.1", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.1.tgz", "integrity": "sha512-hC2v6p8ZSI/W0HUzh3V8C5g+NwSKzKPtJwSpTjwl0o297GP9+ZLQSkdvHz46CM3LqyoXxq+5G9komY+eSqSO0g==", - "dev": true, "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -3783,6 +3783,109 @@ "node": ">=14" } }, + "node_modules/@testing-library/dom": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", + "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/dom/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/dom/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "peer": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/chalk/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "peer": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@testing-library/dom/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "peer": true + }, + "node_modules/@testing-library/dom/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.5.2", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.5.2.tgz", + "integrity": "sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -3814,6 +3917,12 @@ "@types/node": "*" } }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "peer": true + }, "node_modules/@types/babel__code-frame": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/@types/babel__code-frame/-/babel__code-frame-7.0.4.tgz", @@ -4631,7 +4740,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, "engines": { "node": ">=10" }, @@ -4663,6 +4771,15 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "peer": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, "node_modules/array-back": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", @@ -6578,6 +6695,15 @@ "node": ">= 0.6.0" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "peer": true, + "engines": { + "node": ">=6" + } + }, "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", @@ -6641,6 +6767,12 @@ "node": ">=6.0.0" } }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "peer": true + }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -11269,6 +11401,15 @@ "node": ">=12" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/magic-string": { "version": "0.25.9", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", @@ -13736,8 +13877,7 @@ "node_modules/regenerator-runtime": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", - "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==", - "dev": true + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" }, "node_modules/regenerator-transform": { "version": "0.15.2", diff --git a/package.json b/package.json index c0e4fab4..3b3bd3c9 100644 --- a/package.json +++ b/package.json @@ -6,13 +6,17 @@ "scripts": { "test": "npm run wtr && npm run jest", "wtr": "wtr \"./test/**/*.test.(js|html)\" --node-resolve --port=2000 --coverage --concurrent-browsers 4", + "wtr:file": "wtr --node-resolve --port=2000 --coverage --concurrent-browsers 4", "wtr:watch": "npm run wtr -- --watch", + "wtr:file:watch": "npm run wtr:file -- --watch", "int": "wtr \"./test/integration/**/*.int.(js|html)\" --node-resolve --port=2000 --concurrent-browsers 3 --config wtr-integration.config.mjs", "int:watch": "npm run int -- --watch", "int3": "wtr \"./test/integration/**/*.int.(js|html)\" --node-resolve --port=2000 --concurrent-browsers 3 --config wtr-int-browsers.config.mjs", "int3:watch": "npm run int3 -- --watch", "jest": "jest --testPathPattern=test --coverage --coverageDirectory=coverage/jest", + "jest:file": "jest --coverage --coverageDirectory=coverage/jest", "jest:watch": "npm run jest -- --watchAll", + "jest:file:watch": "npm run jest:file -- --watchAll", "lcov": "lcov -a coverage/jest/lcov.info -a coverage/wtr/lcov.info -o coverage/lcov.info", "lint": "npm run lint:js && npm run lint:css", "lint:js": "eslint .", @@ -63,6 +67,7 @@ "dependencies": { "@75lb/deep-merge": "^1.1.2", "@rollup/plugin-replace": "5.0.5", + "@testing-library/user-event": "^14.5.2", "@web/dev-server-rollup": "0.6.1", "@web/test-runner": "0.18.1", "@web/test-runner-commands": "0.9.0", diff --git a/test/blocks/acom-widget/acom-widget-redirect.jest.js b/test/blocks/acom-widget/acom-widget-redirect.jest.js new file mode 100644 index 00000000..a55fd88a --- /dev/null +++ b/test/blocks/acom-widget/acom-widget-redirect.jest.js @@ -0,0 +1,72 @@ +/** + * @jest-environment jsdom + */ +/* eslint-disable compat/compat */ +/* eslint-disable no-undef */ +import path from 'path'; +import fs from 'fs'; +import { userEvent } from '@testing-library/user-event'; + +const mockFetch = jest.fn(() => Promise.resolve({ + json: () => Promise.resolve({ + access_token: '123', + discovery: { + resources: { + jobs: { status: { uri: 'https://pdfnow-dev.adobe.io/status' } }, + assets: { + upload: { uri: 'https://pdfnow-dev.adobe.io/upload' }, + download_uri: { uri: 'https://pdfnow-dev.adobe.io/download' }, + createpdf: { uri: 'https://pdfnow-dev.adobe.io/createpdf' }, + }, + }, + }, + }), + ok: true, +})); + +const xhrMock = { + abort: jest.fn(), + open: jest.fn(), + setRequestHeader: jest.fn(), + onreadystatechange: jest.fn(), + progress: jest.fn(), + upload: new EventTarget(), + send: jest.fn(), + readyState: 4, + responseText: JSON.stringify({ + uri: 'https://www.example.com', + job_uri: 'https://www.example.com/job_uri', + }), + status: 201, +}; + +describe('acom-widget block', () => { + beforeEach(() => { + document.head.innerHTML = fs.readFileSync(path.resolve(__dirname, './mocks/head.html'), 'utf8'); + document.body.innerHTML = fs.readFileSync(path.resolve(__dirname, './mocks/body.html'), 'utf8'); + window.fetch = mockFetch; + window.XMLHttpRequest = jest.fn(() => xhrMock); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('upload PDF', async () => { + const log = jest.spyOn(console, 'log'); + + delete window.location; + window.location = new URL('https://localhost/acrobat/online/ai-chat-pdf.html?redirect=off'); + + const blockModule = await import('../../../acrobat/blocks/acom-widget/acom-widget.js'); + + const block = document.querySelector('.acom-widget'); + await blockModule.default(block); + + const input = document.querySelector('input'); + const file = new File(['hello'], 'hello.png', { type: 'image/png' }); + await userEvent.upload(input, file); + await xhrMock.onreadystatechange(); + expect(log.mock.calls[0][0]).toContain('Blob Viewer URL:'); + }); +}); diff --git a/test/blocks/acom-widget/acom-widget.jest.js b/test/blocks/acom-widget/acom-widget.jest.js new file mode 100644 index 00000000..46fb14a6 --- /dev/null +++ b/test/blocks/acom-widget/acom-widget.jest.js @@ -0,0 +1,81 @@ +/** + * @jest-environment jsdom + */ +/* eslint-disable compat/compat */ +/* eslint-disable no-undef */ +import path from 'path'; +import fs from 'fs'; +import { userEvent } from '@testing-library/user-event'; +import { delay } from '../../helpers/waitfor.js'; +import init from '../../../acrobat/blocks/acom-widget/acom-widget.js'; + +const mockfetch = jest.fn(() => Promise.resolve({ + json: () => Promise.resolve({ + access_token: '123', + discovery: { + resources: { + jobs: { status: { uri: 'https://pdfnow-dev.adobe.io/status' } }, + assets: { + upload: { uri: 'https://pdfnow-dev.adobe.io/upload' }, + download_uri: { uri: 'https://pdfnow-dev.adobe.io/download' }, + createpdf: { uri: 'https://pdfnow-dev.adobe.io/createpdf' }, + }, + }, + }, + }), + ok: true, +})); + +const mockXhr = { + abort: jest.fn(), + open: jest.fn(), + setRequestHeader: jest.fn(), + onreadystatechange: jest.fn(), + progress: jest.fn(), + upload: new EventTarget(), + send: jest.fn(), + readyState: 4, + responseText: JSON.stringify({ + uri: 'https://www.example.com/asseturi/', + job_uri: 'https://www.example.com/job_uri', + }), + status: 201, +}; + +describe('acom-widget block', () => { + beforeEach(() => { + document.head.innerHTML = fs.readFileSync(path.resolve(__dirname, './mocks/head.html'), 'utf8'); + document.body.innerHTML = fs.readFileSync(path.resolve(__dirname, './mocks/body.html'), 'utf8'); + window.fetch = mockfetch; + window.XMLHttpRequest = jest.fn(() => mockXhr); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('upload PDF', async () => { + window.alert = jest.fn(); + + delete window.localStorage.limit; + + delete window.location; + window.location = new URL('https://localhost/acrobat/online/ai-chat-pdf.html'); + + const block = document.querySelector('.acom-widget'); + await init(block); + + const input = document.querySelector('input'); + const file = new File(['hello'], 'hello.png', { type: 'image/png' }); + + window.location = { assign: jest.fn() }; + + await userEvent.upload(input, file); + + await mockXhr.onreadystatechange(); + + await delay(100); + + expect(window.location.href).toMatch(/pdfNowAssetUri=https:\/\/www.example.com\/asseturi\//); + }); +}); diff --git a/test/blocks/acom-widget/acom-widget.test.js b/test/blocks/acom-widget/acom-widget.test.js new file mode 100644 index 00000000..4a6be132 --- /dev/null +++ b/test/blocks/acom-widget/acom-widget.test.js @@ -0,0 +1,153 @@ +/* eslint-disable compat/compat */ +import { readFile } from '@web/test-runner-commands'; +import { expect } from '@esm-bundle/chai'; +import sinon from 'sinon'; +import { delay, waitForElement } from '../../helpers/waitfor.js'; + +const { default: init } = await import( + '../../../acrobat/blocks/acom-widget/acom-widget.js' +); + +const uploadFile = (input, file) => { + const changeEvent = new Event('change'); + Object.defineProperty(changeEvent, 'target', { writable: false, value: { files: [file] } }); + input.dispatchEvent(changeEvent); +}; + +describe('acom-widget block', () => { + let xhr; + + beforeEach(async () => { + sinon.stub(window, 'fetch'); + window.fetch.callsFake((x) => { + if (x === 'https://pdfnow-dev.adobe.io/status') { + return Promise.resolve({ ok: false }); + } + return Promise.resolve({ + json: () => Promise.resolve({ + access_token: '123', + discovery: { + resources: { + jobs: { status: { uri: 'https://pdfnow-dev.adobe.io/status' } }, + assets: { + upload: { uri: 'https://pdfnow-dev.adobe.io/upload' }, + download_uri: { uri: 'https://pdfnow-dev.adobe.io/download' }, + createpdf: { uri: 'https://pdfnow-dev.adobe.io/createpdf' }, + }, + }, + }, + }), + ok: true, + }); + }); + xhr = sinon.useFakeXMLHttpRequest(); + document.head.innerHTML = await readFile({ path: './mocks/head.html' }); + document.body.innerHTML = await readFile({ path: './mocks/body.html' }); + delete window.localStorage.limit; + }); + + afterEach(() => { + xhr.restore(); + sinon.restore(); + }); + + it('reach limit', async () => { + window.localStorage.limit = 2; + + const block = document.body.querySelector('.acom-widget'); + await init(block); + + expect(document.querySelector('.upsell')).to.exist; + }); + + it('upload invalid file', async () => { + const alert = sinon.stub(window, 'alert').callsFake(() => {}); + + const block = document.querySelector('.acom-widget'); + await init(block); + + const input = document.querySelector('input'); + const file = new File(['hello'], 'hello.txt', { type: 'text/plain' }); + + uploadFile(input, file); + + expect(alert.getCall(0).args[0]).to.eq('This file is invalid'); + }); + + it('cancel upload', async () => { + sinon.stub(window, 'alert').callsFake(() => {}); + + const block = document.querySelector('.acom-widget'); + await init(block); + + const input = document.querySelector('input'); + const file = new File(['hello'], 'hello.png', { type: 'image/png' }); + + uploadFile(input, file); + + await delay(1000); + + document.querySelector('.widget-cancel').click(); + + const upload = await waitForElement('#file-upload'); + expect(upload).to.be.exist; + }); + + it('SSRF check', async () => { + window.fetch.restore(); + sinon.stub(window, 'fetch'); + window.fetch.returns(Promise.resolve({ + json: () => Promise.resolve({ + access_token: '123', + discovery: { resources: { assets: { upload: { uri: 'https://example.com/upload' } } } }, + }), + ok: true, + })); + sinon.stub(window, 'alert').callsFake(() => {}); + + const block = document.querySelector('.acom-widget'); + await init(block); + + const input = document.querySelector('input'); + const file = new File(['hello'], 'hello.png', { type: 'image/png' }); + + uploadFile(input, file); + + await delay(500); + + expect(alert.getCall(0).args[0]).to.eq('An error occurred during the upload process. Please try again.'); + }); + + it('upload PNG and fail at job status', async () => { + sinon.stub(window, 'alert').callsFake(() => {}); + + const requests = []; + + xhr.onCreate = (x) => { + requests.push(x); + }; + + const block = document.body.querySelector('.acom-widget'); + await init(block); + + const input = document.querySelector('input'); + const file = new File(['hello'], 'hello.png', { type: 'image/png' }); + + uploadFile(input, file); + + await delay(500); + + requests[0].respond( + 201, + { 'Content-Type': 'application/json' }, + JSON.stringify({ + uri: 'https://www.example.com/product', + job_uri: 'https://www.example.com/job_uri', + }), + ); + + await delay(500); + + expect(alert.getCall(0).args[0]).to.eq('Failed to create PDF'); + }); +}); diff --git a/test/blocks/acom-widget/mocks/body.html b/test/blocks/acom-widget/mocks/body.html new file mode 100644 index 00000000..6fa3246d --- /dev/null +++ b/test/blocks/acom-widget/mocks/body.html @@ -0,0 +1,9 @@ +
+
+
pdf-to-ppt
+
Heading
+
Copy
+
Label
+
Legal
+
+
diff --git a/test/blocks/acom-widget/mocks/head.html b/test/blocks/acom-widget/mocks/head.html new file mode 100644 index 00000000..be4425f0 --- /dev/null +++ b/test/blocks/acom-widget/mocks/head.html @@ -0,0 +1,3 @@ + + + \ No newline at end of file