From af4c6537dd2d05f9642f991685e93843c2749b51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jim=20Br=C3=A4nnlund?= Date: Fri, 28 Jul 2023 17:28:14 +0200 Subject: [PATCH] Fix: Collapsed state between redraws (#703) --- package-lock.json | 88 ++++++++++++++++++++++++++ package.json | 3 +- src/pytest_html/scripts/datamanager.js | 14 +++- src/pytest_html/scripts/filter.js | 4 ++ src/pytest_html/scripts/main.js | 34 +++++++++- src/pytest_html/scripts/sort.js | 19 ++++-- src/pytest_html/scripts/storage.js | 16 +++-- testing/unittest.js | 17 ++++- 8 files changed, 175 insertions(+), 20 deletions(-) diff --git a/package-lock.json b/package-lock.json index ccaec456..68b0215c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "eslint": "^8.20.0", "eslint-config-google": "^0.14.0", "mocha": "^10.0.0", + "mock-local-storage": "^1.1.24", "nyc": "^15.1.0", "sass": "^1.52.3", "sinon": "^14.0.0" @@ -1497,6 +1498,17 @@ "integrity": "sha512-Y8L5rp6jo+g9VEPgvqNfEopjTR4OTYct8lXlS8iVQdmnjDvbdbzYe9rjtFCB9egC86JoNCU61WRY+ScjkZpnIg==", "dev": true }, + "node_modules/core-js": { + "version": "3.32.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.32.0.tgz", + "integrity": "sha512-rd4rYZNlF3WuoYuRIDEmbR/ga9CeuWX9U05umAvgrrZoHY4Z++cp/xwPQMvUpBB4Ag6J8KfD80G0zwCyaSxDww==", + "dev": true, + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -1739,6 +1751,12 @@ "node": ">=6.0.0" } }, + "node_modules/dom-walk": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", + "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==", + "dev": true + }, "node_modules/domain-browser": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", @@ -2300,6 +2318,16 @@ "node": ">=10.13.0" } }, + "node_modules/global": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", + "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==", + "dev": true, + "dependencies": { + "min-document": "^2.19.0", + "process": "^0.11.10" + } + }, "node_modules/globals": { "version": "13.20.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", @@ -3145,6 +3173,15 @@ "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", "dev": true }, + "node_modules/min-document": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", + "integrity": "sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==", + "dev": true, + "dependencies": { + "dom-walk": "^0.1.0" + } + }, "node_modules/minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", @@ -3298,6 +3335,16 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/mock-local-storage": { + "version": "1.1.24", + "resolved": "https://registry.npmjs.org/mock-local-storage/-/mock-local-storage-1.1.24.tgz", + "integrity": "sha512-NEfmw+yEK9oe6xCfOnTaJ6Dz+L3eu6vsZopJlxflXYxr7Mg3EV+S0NXKUQlY9AAeDEdaPZDSUGq1Gi6kLSa5PA==", + "dev": true, + "dependencies": { + "core-js": "^3.30.2", + "global": "^4.3.2" + } + }, "node_modules/module-deps": { "version": "6.2.3", "resolved": "https://registry.npmjs.org/module-deps/-/module-deps-6.2.3.tgz", @@ -6172,6 +6219,12 @@ "integrity": "sha512-Y8L5rp6jo+g9VEPgvqNfEopjTR4OTYct8lXlS8iVQdmnjDvbdbzYe9rjtFCB9egC86JoNCU61WRY+ScjkZpnIg==", "dev": true }, + "core-js": { + "version": "3.32.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.32.0.tgz", + "integrity": "sha512-rd4rYZNlF3WuoYuRIDEmbR/ga9CeuWX9U05umAvgrrZoHY4Z++cp/xwPQMvUpBB4Ag6J8KfD80G0zwCyaSxDww==", + "dev": true + }, "core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -6371,6 +6424,12 @@ "esutils": "^2.0.2" } }, + "dom-walk": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", + "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==", + "dev": true + }, "domain-browser": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", @@ -6791,6 +6850,16 @@ "is-glob": "^4.0.3" } }, + "global": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", + "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==", + "dev": true, + "requires": { + "min-document": "^2.19.0", + "process": "^0.11.10" + } + }, "globals": { "version": "13.20.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", @@ -7425,6 +7494,15 @@ } } }, + "min-document": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", + "integrity": "sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==", + "dev": true, + "requires": { + "dom-walk": "^0.1.0" + } + }, "minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", @@ -7549,6 +7627,16 @@ } } }, + "mock-local-storage": { + "version": "1.1.24", + "resolved": "https://registry.npmjs.org/mock-local-storage/-/mock-local-storage-1.1.24.tgz", + "integrity": "sha512-NEfmw+yEK9oe6xCfOnTaJ6Dz+L3eu6vsZopJlxflXYxr7Mg3EV+S0NXKUQlY9AAeDEdaPZDSUGq1Gi6kLSa5PA==", + "dev": true, + "requires": { + "core-js": "^3.30.2", + "global": "^4.3.2" + } + }, "module-deps": { "version": "6.2.3", "resolved": "https://registry.npmjs.org/module-deps/-/module-deps-6.2.3.tgz", diff --git a/package.json b/package.json index e8b23125..8b5bcaeb 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "build:css": "sass --no-source-map --no-error-css src/layout/css/style.scss src/pytest_html/resources/style.css", "build:jsapp": "browserify ./src/pytest_html/scripts/index.js > ./src/pytest_html/resources/app.js", "lint": "eslint src/pytest_html/scripts/ testing/", - "unit": "nyc mocha testing/**/unittest.js", + "unit": "nyc mocha testing/**/unittest.js --require mock-local-storage", "all": "npm run lint && npm run unit && npm run build:css && npm run build:jsapp" }, "devDependencies": { @@ -13,6 +13,7 @@ "eslint": "^8.20.0", "eslint-config-google": "^0.14.0", "mocha": "^10.0.0", + "mock-local-storage": "^1.1.24", "nyc": "^15.1.0", "sass": "^1.52.3", "sinon": "^14.0.0" diff --git a/src/pytest_html/scripts/datamanager.js b/src/pytest_html/scripts/datamanager.js index 4855818c..b95e95d9 100644 --- a/src/pytest_html/scripts/datamanager.js +++ b/src/pytest_html/scripts/datamanager.js @@ -1,19 +1,25 @@ -const { getCollapsedCategory } = require('./storage.js') +const { getCollapsedCategory, setCollapsedIds } = require('./storage.js') class DataManager { setManager(data) { const collapsedCategories = [...getCollapsedCategory(data.renderCollapsed)] + const collapsedIds = [] const tests = Object.values(data.tests).flat().map((test, index) => { const collapsed = collapsedCategories.includes(test.result.toLowerCase()) + const id = `test_${index}` + if (collapsed) { + collapsedIds.push(id) + } return { ...test, - id: `test_${index}`, + id, collapsed, } }) const dataBlob = { ...data, tests } this.data = { ...dataBlob } this.renderData = { ...dataBlob } + setCollapsedIds(collapsedIds) } get allData() { @@ -47,6 +53,10 @@ class DataManager { get environment() { return this.renderData.environment } + + get initialSort() { + return this.data.initialSort + } } module.exports = { diff --git a/src/pytest_html/scripts/filter.js b/src/pytest_html/scripts/filter.js index e9d384e4..abc028f6 100644 --- a/src/pytest_html/scripts/filter.js +++ b/src/pytest_html/scripts/filter.js @@ -1,4 +1,5 @@ const { manager } = require('./datamanager.js') +const { doSort } = require('./sort.js') const storageModule = require('./storage.js') const getFilteredSubSet = (filter) => @@ -20,6 +21,9 @@ const doFilter = (type, show) => { const currentFilter = storageModule.getVisible() const filteredSubset = getFilteredSubSet(currentFilter) manager.setRender(filteredSubset) + + const sortColumn = storageModule.getSort() + doSort(sortColumn, true) } module.exports = { diff --git a/src/pytest_html/scripts/main.js b/src/pytest_html/scripts/main.js index 4fe30f7d..f675dae4 100644 --- a/src/pytest_html/scripts/main.js +++ b/src/pytest_html/scripts/main.js @@ -2,7 +2,14 @@ const { dom, findAll } = require('./dom.js') const { manager } = require('./datamanager.js') const { doSort } = require('./sort.js') const { doFilter } = require('./filter.js') -const { getVisible, getSort, getSortDirection, possibleFilters } = require('./storage.js') +const { + getVisible, + getCollapsedIds, + setCollapsedIds, + getSort, + getSortDirection, + possibleFilters, +} = require('./storage.js') const removeChildren = (node) => { while (node.firstChild) { @@ -22,7 +29,7 @@ const renderStatic = () => { } const renderContent = (tests) => { - const sortAttr = getSort(manager.allData.initialSort) + const sortAttr = getSort(manager.initialSort) const sortAsc = JSON.parse(getSortDirection()) const rows = tests.map(dom.getResultTBody) const table = document.getElementById('results-table') @@ -53,7 +60,17 @@ const renderContent = (tests) => { findAll('.collapsible td:not(.col-links').forEach((elem) => { elem.addEventListener('click', ({ target }) => { - manager.toggleCollapsedItem(target.parentElement.dataset.id) + const id = target.parentElement.dataset.id + manager.toggleCollapsedItem(id) + + const collapsedIds = getCollapsedIds() + if (collapsedIds.includes(id)) { + const updated = collapsedIds.filter((item) => item !== id) + setCollapsedIds(updated) + } else { + collapsedIds.push(id) + setCollapsedIds(collapsedIds) + } redraw() }) }) @@ -73,6 +90,14 @@ const bindEvents = () => { const { testResult } = element.dataset doFilter(testResult, element.checked) + const collapsedIds = getCollapsedIds() + const updated = manager.renderData.tests.map((test) => { + return { + ...test, + collapsed: collapsedIds.includes(test.id), + } + }) + manager.setRender(updated) redraw() } @@ -88,10 +113,13 @@ const bindEvents = () => { }) document.getElementById('show_all_details').addEventListener('click', () => { manager.allCollapsed = false + setCollapsedIds([]) redraw() }) document.getElementById('hide_all_details').addEventListener('click', () => { manager.allCollapsed = true + const allIds = manager.renderData.tests.map((test) => test.id) + setCollapsedIds(allIds) redraw() }) } diff --git a/src/pytest_html/scripts/sort.js b/src/pytest_html/scripts/sort.js index 83e1340c..bc94147a 100644 --- a/src/pytest_html/scripts/sort.js +++ b/src/pytest_html/scripts/sort.js @@ -43,10 +43,14 @@ const durationSort = (list, ascending) => { } const doInitSort = () => { - const type = storageModule.getSort(manager.allData.initialSort) + const type = storageModule.getSort(manager.initialSort) const ascending = storageModule.getSortDirection() const list = manager.testSubset const initialOrder = ['Error', 'Failed', 'Rerun', 'XFailed', 'XPassed', 'Skipped', 'Passed'] + + storageModule.setSort(type) + storageModule.setSortDirection(ascending) + if (type?.toLowerCase() === 'original') { manager.setRender(list) } else { @@ -66,14 +70,19 @@ const doInitSort = () => { } } -const doSort = (type) => { - const newSortType = storageModule.getSort(manager.allData.initialSort) !== type +const doSort = (type, skipDirection) => { + const newSortType = storageModule.getSort(manager.initialSort) !== type const currentAsc = storageModule.getSortDirection() - const ascending = newSortType ? true : !currentAsc + let ascending + if (skipDirection) { + ascending = currentAsc + } else { + ascending = newSortType ? false : !currentAsc + } storageModule.setSort(type) storageModule.setSortDirection(ascending) - const list = manager.testSubset + const list = manager.testSubset const sortedList = type === 'duration' ? durationSort(list, ascending) : genericSort(list, type, ascending) manager.setRender(sortedList) } diff --git a/src/pytest_html/scripts/storage.js b/src/pytest_html/scripts/storage.js index fdcc9882..ac39c12b 100644 --- a/src/pytest_html/scripts/storage.js +++ b/src/pytest_html/scripts/storage.js @@ -30,7 +30,7 @@ const hideCategory = (categoryToHide) => { const settings = [...new Set(currentVisible)].filter((f) => f !== categoryToHide).join(',') url.searchParams.set('visible', settings) - history.pushState({}, null, unescape(url.href)) + window.history.pushState({}, null, unescape(url.href)) } const showCategory = (categoryToShow) => { @@ -44,7 +44,7 @@ const showCategory = (categoryToShow) => { const noFilter = possibleFilters.length === settings.length || !settings.length noFilter ? url.searchParams.delete('visible') : url.searchParams.set('visible', settings.join(',')) - history.pushState({}, null, unescape(url.href)) + window.history.pushState({}, null, unescape(url.href)) } const getSort = (initialSort) => { @@ -59,7 +59,7 @@ const getSort = (initialSort) => { const setSort = (type) => { const url = new URL(window.location.href) url.searchParams.set('sort', type) - history.pushState({}, null, unescape(url.href)) + window.history.pushState({}, null, unescape(url.href)) } const getCollapsedCategory = (renderCollapsed) => { @@ -87,17 +87,21 @@ const getCollapsedCategory = (renderCollapsed) => { return categories } -const getSortDirection = () => JSON.parse(sessionStorage.getItem('sortAsc')) - +const getSortDirection = () => JSON.parse(sessionStorage.getItem('sortAsc')) || false const setSortDirection = (ascending) => sessionStorage.setItem('sortAsc', ascending) +const getCollapsedIds = () => JSON.parse(sessionStorage.getItem('collapsedIds')) || [] +const setCollapsedIds = (list) => sessionStorage.setItem('collapsedIds', JSON.stringify(list)) + module.exports = { getVisible, hideCategory, showCategory, + getCollapsedIds, + setCollapsedIds, getSort, - getSortDirection, setSort, + getSortDirection, setSortDirection, getCollapsedCategory, possibleFilters, diff --git a/testing/unittest.js b/testing/unittest.js index 36229787..dc17ecc0 100644 --- a/testing/unittest.js +++ b/testing/unittest.js @@ -44,6 +44,9 @@ const mockWindow = (queryParam) => { location: { href: `https://example.com/page?${queryParam}`, }, + history: { + pushState: sinon.stub(), + }, } originalWindow = global.window global.window = mock @@ -76,12 +79,17 @@ describe('Filter tests', () => { }) }) describe('doFilter', () => { + let originalWindow + + after(() => global.window = originalWindow) + it('removes all but passed', () => { + mockWindow() getFilterMock = sinon.stub(storageModule, 'getVisible').returns(['passed']) managerSpy = sinon.spy(dataModule.manager, 'setRender') doFilter('passed', true) - expect(managerSpy.callCount).to.eql(1) + expect(managerSpy.callCount).to.eql(2) expect(dataModule.manager.testSubset.map(({ result }) => result)).to.eql([ 'passed', 'passed', 'passed', 'passed', 'passed', ]) @@ -134,9 +142,12 @@ describe('Sort tests', () => { let managerSpy let sortMock let sortDirectionMock - beforeEach(() => dataModule.manager.resetRender()) + let originalWindow + before(() => mockWindow()) + beforeEach(() => dataModule.manager.resetRender()) afterEach(() => [sortMock, sortDirectionMock, managerSpy].forEach((fn) => fn.restore())) + after(() => global.window = originalWindow) it('has no stored sort', () => { sortMock = sinon.stub(storageModule, 'getSort').returns('result') sortDirectionMock = sinon.stub(storageModule, 'getSortDirection').returns(null) @@ -191,7 +202,7 @@ describe('Sort tests', () => { doSort('result') expect(managerSpy.callCount).to.eql(1) expect(dataModule.manager.testSubset.map(({ result }) => result)).to.eql([ - 'passed', 'passed', 'passed', 'passed', 'passed', 'failed', + 'failed', 'passed', 'passed', 'passed', 'passed', 'passed', ]) }) })