Repository navigation
✨ Allow user-defined sorting or grouping - #1089
Merged
Merged
Conversation
gwennlbh
force-pushed
the
custom-group-by-sort-by
branch
5 times, most recently
from
January 17, 2026 19:47
be22818 to
26d8f54
Compare
gwennlbh
had a problem deploying
to
#1089: Unit tests report
January 17, 2026 19:48 — with
GitHub Actions
Failure
gwennlbh
temporarily deployed
to
#1089: App preview
January 17, 2026 19:48 — with
GitHub Actions
Inactive
gwennlbh
force-pushed
the
custom-group-by-sort-by
branch
from
January 18, 2026 00:11
26d8f54 to
9a1e71e
Compare
gwennlbh
had a problem deploying
to
#1089: Unit tests report
January 18, 2026 00:12 — with
GitHub Actions
Failure
gwennlbh
had a problem deploying
to
#1089: App preview
January 18, 2026 00:12 — with
GitHub Actions
Error
gwennlbh
force-pushed
the
custom-group-by-sort-by
branch
from
January 18, 2026 00:13
9a1e71e to
9917b9b
Compare
gwennlbh
had a problem deploying
to
#1089: Unit tests report
January 18, 2026 00:15 — with
GitHub Actions
Failure
gwennlbh
had a problem deploying
to
#1089: App preview
January 18, 2026 00:15 — with
GitHub Actions
Error
gwennlbh
force-pushed
the
custom-group-by-sort-by
branch
from
January 18, 2026 00:17
9917b9b to
61f1f88
Compare
gwennlbh
temporarily deployed
to
#1089: App preview
January 18, 2026 00:19 — with
GitHub Actions
Inactive
gwennlbh
had a problem deploying
to
#1089: Unit tests report
January 18, 2026 00:19 — with
GitHub Actions
Failure
gwennlbh
force-pushed
the
custom-group-by-sort-by
branch
from
January 18, 2026 17:33
61f1f88 to
2d35ecf
Compare
gwennlbh
had a problem deploying
to
#1089: Unit tests report
January 18, 2026 18:39 — with
GitHub Actions
Failure
gwennlbh
temporarily deployed
to
#1089: App preview
January 18, 2026 18:39 — with
GitHub Actions
Inactive
gwennlbh
temporarily deployed
to
#1089: App preview
January 18, 2026 18:52 — with
GitHub Actions
Inactive
gwennlbh
had a problem deploying
to
#1089: Unit tests report
January 18, 2026 18:52 — with
GitHub Actions
Failure
gwennlbh
force-pushed
the
custom-group-by-sort-by
branch
from
January 18, 2026 18:56
8e5f932 to
ffa16d5
Compare
gwennlbh
temporarily deployed
to
#1089: Unit tests report
January 18, 2026 18:57 — with
GitHub Actions
Inactive
gwennlbh
temporarily deployed
to
#1089: App preview
January 18, 2026 18:57 — with
GitHub Actions
Inactive
gwennlbh
temporarily deployed
to
#1089: Unit tests report
January 18, 2026 18:58 — with
GitHub Actions
Inactive
gwennlbh
temporarily deployed
to
#1089: Test coverage report
January 18, 2026 18:59 — with
GitHub Actions
Inactive
Contributor
|
✅ Coverage is up by 0.66% (from 43.66% to 44.32%) |
Contributor
Bundle StatsHey there, this message comes from a GitHub action that helps you and reviewers to understand how these changes affect the size of this project's bundle. As this PR is updated, I'll keep you updated on how the bundle size is impacted. Total
Changeset No files were changed View detailed bundle breakdownAdded
Removed
Bigger
Smaller
|
gwennlbh
temporarily deployed
to
#1089: End-to-end tests report
January 18, 2026 19:10 — with
GitHub Actions
Inactive
gwennlbh
added a commit
that referenced
this pull request
Jan 18, 2026
cuz we'll make it generate sourcemaps
Not needed after all (we just had to parse more date formats when deserializing from database, namely fractional seconds), but could be useful in the future: diff --git a/package.json b/package.json index 8c1357b..9c7c19650 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,8 @@ "test:ui": "vitest --ui", "test:watch": "vitest", "wuchale": "wuchale", - "all-contributors": "all-contributors --files README.md --files README_FR.md" + "all-contributors": "all-contributors --files README.md --files README_FR.md", + "test:update-fixtures": "playwright test tasks/exports.spec.js && playwright test tasks/dbdump.spec.js && playwright --update-snapshots '\\w+.spec.(js|ts)'" }, "devDependencies": { "@actions/core": "^2.0.2", diff --git a/tests/tasks/dbdump.spec.js b/tests/tasks/dbdump.spec.js index 464e91f..329e329e8 100644 --- a/tests/tasks/dbdump.spec.js +++ b/tests/tasks/dbdump.spec.js @@ -1,6 +1,3 @@ -import { readFile, writeFile } from 'node:fs/promises'; -import JSZip from 'jszip'; - import { exampleProtocol, expect, test } from '../fixtures.js'; import { confirmDeletionModal, @@ -9,8 +6,7 @@ import { importPhotos, importProtocol, importResults, - newSession, - sessionMetadataSectionFor + newSession } from '../utils.js'; test.skip( @@ -18,162 +14,28 @@ test.skip( 'Skipping database dumps and exports on CI, these are meant as easy way to (re)create dump fixtures locally only.' ); -test.describe('Database dumps', () => { - test('basic', async ({ page }) => { - await goToProtocolManagement(page); - await importProtocol(page, 'arthropods.light.cigaleprotocol.json'); - - await importResults(page, 'exports/correct.zip'); - await dumpDatabase(page, 'basic.devalue'); - }); +test('basic', async ({ page }) => { + await goToProtocolManagement(page); + await importProtocol(page, 'arthropods.light.cigaleprotocol.json'); - test('kitchensink-protocol', async ({ page, app }) => { - await goToProtocolManagement(page); - await importProtocol(page, 'kitchensink.cigaleprotocol.yaml'); - await page - .getByRole('listitem') - .filter({ hasText: exampleProtocol.id }) - .getByRole('button', { name: 'Supprimer' }) - .click(); - await confirmDeletionModal(page, { type: exampleProtocol.name }); - await expect(page.getByText('Protocole supprimé')).toBeVisible(); - await newSession(page); - - await app.tabs.go('import'); - await importPhotos({ page }, 'cyan.jpeg', 'leaf.jpeg'); - await page.waitForTimeout(2_000); - await dumpDatabase(page, 'kitchensink-protocol.devalue'); - }); + await importResults(page, 'exports/correct.zip'); + await dumpDatabase(page, 'basic.devalue'); }); -test.describe('Exports', () => { - /** - * @PARAM {object} param0 - * @PARAM {import('@playwright/test').Page} param0.page - * @PARAM {import('../fixtures.js').AppFixture} param0.app - */ - async function prepare({ page, app }) { - await newSession(page, { name: 'Testing session' }); - - await page.getByTestId('goto-current-session').click(); - await page.waitForURL((u) => u.hash.startsWith('#/sessions/')); - await sessionMetadataSectionFor(page, 'Durée de prospection') - .getByRole('textbox') - .first() - .fill('54'); - await sessionMetadataSectionFor(page, 'Durée de prospection') - .getByRole('textbox') - .first() - .blur(); - await sessionMetadataSectionFor(page, 'Vent') - .getByRole('radiogroup') - .getByRole('radio', { name: 'Modéré' }) - .check(); - - await app.tabs.go('import'); - await importPhotos( - { page }, - 'cyan.jpeg', - 'leaf.jpeg', - 'lil-fella.jpeg', - 'with-exif-gps.jpeg' - ); - await expect(page.getByText(/Analyse….|En attente/)).toHaveCount(0, { - timeout: 30_000 - }); - - await app.tabs.go('crop'); - await page.getByText('lil-fella.jpeg').click(); - await page.getByRole('button', { name: 'Continuer' }).click(); - await page.waitForTimeout(1000); - await page.getByRole('button', { name: 'Autres photos Esc' }).click(); - - await app.tabs.go('classify'); - await page.waitForTimeout(1000); - await expect(page.getByText('Chargement du modèle de classification')).toHaveCount(0, { - timeout: 10_000 - }); - await page.waitForTimeout(5_000); - await expect(page.getByText(/Analyse…|En attente/)).toHaveCount(0, { timeout: 10_000 }); - } - - test('correct', async ({ page, app }) => { - await prepare({ page, app }); - await app.tabs.go('results'); - await page.getByText('Métadonnées, images recadrées et images originales').click(); - await page.getByRole('button', { name: 'results.zip' }).click(); - const download = await page.waitForEvent('download'); - await download.saveAs('./tests/fixtures/exports/correct.zip'); - }); - - test('no-originals', async ({ page, app }) => { - await prepare({ page, app }); - await app.tabs.go('results'); - await page.getByText('Métadonnées et images recadrées').click(); - await page.getByRole('button', { name: 'results.zip' }).click(); - const download = await page.waitForEvent('download'); - await download.saveAs('./tests/fixtures/exports/no-originals.zip'); - }); - - test('no-analysis', async ({ page, app }) => { - await prepare({ page, app }); - await app.tabs.go('results'); - await page.getByText('Métadonnées, images recadrées et images originales').click(); - await page.getByRole('button', { name: 'results.zip' }).click(); - const download = await page.waitForEvent('download'); - const zipPath = './tests/fixtures/exports/no-analysis.zip'; - await download.saveAs(zipPath); - - // Remove analysis.json from the zip - const zipData = await readFile(zipPath); - const zip = await JSZip.loadAsync(zipData); - zip.remove('analysis.json'); - const newZipData = await zip.generateAsync({ type: 'nodebuffer' }); - await writeFile(zipPath, newZipData); - }); - - test('invalid-json-analysis', async ({ page, app }) => { - await prepare({ page, app }); - await app.tabs.go('results'); - await page.getByText('Métadonnées, images recadrées et images originales').click(); - await page.getByRole('button', { name: 'results.zip' }).click(); - const download = await page.waitForEvent('download'); - const zipPath = './tests/fixtures/exports/invalid-json-analysis.zip'; - await download.saveAs(zipPath); - - // Erase first '{' from analysis.json - const zipData = await readFile(zipPath); - const zip = await JSZip.loadAsync(zipData); - const analysisFile = zip.file('analysis.json'); - if (!analysisFile) throw new Error('analysis.json not found in zip'); - const analysisContent = await analysisFile.async('string'); - const modifiedContent = analysisContent.replace('{', ''); - zip.file('analysis.json', modifiedContent); - const newZipData = await zip.generateAsync({ type: 'nodebuffer' }); - await writeFile(zipPath, newZipData); - }); - - test('wrong-protocol', async ({ page, app }) => { - await prepare({ page, app }); - await app.tabs.go('results'); - await page.getByText('Métadonnées, images recadrées et images originales').click(); - await page.getByRole('button', { name: 'results.zip' }).click(); - const download = await page.waitForEvent('download'); - await download.saveAs('./tests/fixtures/exports/wrong-protocol.zip'); - - // Change protocol ID in analysis.json - const zipPath = './tests/fixtures/exports/wrong-protocol.zip'; - const zipData = await readFile(zipPath); - const zip = await JSZip.loadAsync(zipData); - const analysisFile = zip.file('analysis.json'); - if (!analysisFile) throw new Error('analysis.json not found in zip'); - const analysisContent = await analysisFile.async('string'); - const modifiedContent = analysisContent.replace( - '"protocol": "io.github.cigaleapp.arthropods.example.light"', - '"protocol": "io.github.cigaleapp.arthropods.example.wrong"' - ); - zip.file('analysis.json', modifiedContent); - const newZipData = await zip.generateAsync({ type: 'nodebuffer' }); - await writeFile(zipPath, newZipData); - }); +test('kitchensink-protocol', async ({ page, app }) => { + await goToProtocolManagement(page); + await importProtocol(page, 'kitchensink.cigaleprotocol.yaml'); + await page + .getByRole('listitem') + .filter({ hasText: exampleProtocol.id }) + .getByRole('button', { name: 'Supprimer' }) + .click(); + await confirmDeletionModal(page, { type: exampleProtocol.name }); + await expect(page.getByText('Protocole supprimé')).toBeVisible(); + await newSession(page); + + await app.tabs.go('import'); + await importPhotos({ page }, 'cyan.jpeg', 'leaf.jpeg'); + await page.waitForTimeout(2_000); + await dumpDatabase(page, 'kitchensink-protocol.devalue'); }); diff --git a/tests/tasks/exports.spec.js b/tests/tasks/exports.spec.js new file mode 100644 index 000000000..5ced92a59 --- /dev/null +++ b/tests/tasks/exports.spec.js @@ -0,0 +1,134 @@ +import { readFile, writeFile } from 'node:fs/promises'; +import JSZip from 'jszip'; + +import { expect, test } from '../fixtures.js'; +import { importPhotos, newSession, sessionMetadataSectionFor } from '../utils.js'; + +test.skip( + Boolean(process.env.CI), + 'Skipping database dumps and exports on CI, these are meant as easy way to (re)create dump fixtures locally only.' +); + +/** + * @PARAM {object} param0 + * @PARAM {import('@playwright/test').Page} param0.page + * @PARAM {import('../fixtures.js').AppFixture} param0.app + */ +async function prepare({ page, app }) { + await newSession(page, { name: 'Testing session' }); + + await page.getByTestId('goto-current-session').click(); + await page.waitForURL((u) => u.hash.startsWith('#/sessions/')); + await sessionMetadataSectionFor(page, 'Durée de prospection') + .getByRole('textbox') + .first() + .fill('54'); + await sessionMetadataSectionFor(page, 'Durée de prospection') + .getByRole('textbox') + .first() + .blur(); + await sessionMetadataSectionFor(page, 'Vent') + .getByRole('radiogroup') + .getByRole('radio', { name: 'Modéré' }) + .check(); + + await app.tabs.go('import'); + await importPhotos({ page }, 'cyan.jpeg', 'leaf.jpeg', 'lil-fella.jpeg', 'with-exif-gps.jpeg'); + await expect(page.getByText(/Analyse….|En attente/)).toHaveCount(0, { + timeout: 30_000 + }); + + await app.tabs.go('crop'); + await page.getByText('lil-fella.jpeg').click(); + await page.getByRole('button', { name: 'Continuer' }).click(); + await page.waitForTimeout(1000); + await page.getByRole('button', { name: 'Autres photos Esc' }).click(); + + await app.tabs.go('classify'); + await page.waitForTimeout(1000); + await expect(page.getByText('Chargement du modèle de classification')).toHaveCount(0, { + timeout: 10_000 + }); + await page.waitForTimeout(5_000); + await expect(page.getByText(/Analyse…|En attente/)).toHaveCount(0, { timeout: 10_000 }); +} + +test('correct', async ({ page, app }) => { + await prepare({ page, app }); + await app.tabs.go('results'); + await page.getByText('Métadonnées, images recadrées et images originales').click(); + await page.getByRole('button', { name: 'results.zip' }).click(); + const download = await page.waitForEvent('download'); + await download.saveAs('./tests/fixtures/exports/correct.zip'); +}); + +test('no-originals', async ({ page, app }) => { + await prepare({ page, app }); + await app.tabs.go('results'); + await page.getByText('Métadonnées et images recadrées').click(); + await page.getByRole('button', { name: 'results.zip' }).click(); + const download = await page.waitForEvent('download'); + await download.saveAs('./tests/fixtures/exports/no-originals.zip'); +}); + +test('no-analysis', async ({ page, app }) => { + await prepare({ page, app }); + await app.tabs.go('results'); + await page.getByText('Métadonnées, images recadrées et images originales').click(); + await page.getByRole('button', { name: 'results.zip' }).click(); + const download = await page.waitForEvent('download'); + const zipPath = './tests/fixtures/exports/no-analysis.zip'; + await download.saveAs(zipPath); + + // Remove analysis.json from the zip + const zipData = await readFile(zipPath); + const zip = await JSZip.loadAsync(zipData); + zip.remove('analysis.json'); + const newZipData = await zip.generateAsync({ type: 'nodebuffer' }); + await writeFile(zipPath, newZipData); +}); + +test('invalid-json-analysis', async ({ page, app }) => { + await prepare({ page, app }); + await app.tabs.go('results'); + await page.getByText('Métadonnées, images recadrées et images originales').click(); + await page.getByRole('button', { name: 'results.zip' }).click(); + const download = await page.waitForEvent('download'); + const zipPath = './tests/fixtures/exports/invalid-json-analysis.zip'; + await download.saveAs(zipPath); + + // Erase first '{' from analysis.json + const zipData = await readFile(zipPath); + const zip = await JSZip.loadAsync(zipData); + const analysisFile = zip.file('analysis.json'); + if (!analysisFile) throw new Error('analysis.json not found in zip'); + const analysisContent = await analysisFile.async('string'); + const modifiedContent = analysisContent.replace('{', ''); + zip.file('analysis.json', modifiedContent); + const newZipData = await zip.generateAsync({ type: 'nodebuffer' }); + await writeFile(zipPath, newZipData); +}); + +test('wrong-protocol', async ({ page, app }) => { + await prepare({ page, app }); + await app.tabs.go('results'); + await page.getByText('Métadonnées, images recadrées et images originales').click(); + await page.getByRole('button', { name: 'results.zip' }).click(); + const download = await page.waitForEvent('download'); + await download.saveAs('./tests/fixtures/exports/wrong-protocol.zip'); + + // Change protocol ID in analysis.json + const zipPath = './tests/fixtures/exports/wrong-protocol.zip'; + const zipData = await readFile(zipPath); + const zip = await JSZip.loadAsync(zipData); + const analysisFile = zip.file('analysis.json'); + if (!analysisFile) throw new Error('analysis.json not found in zip'); + const analysisContent = await analysisFile.async('string'); + const modifiedContent = analysisContent.replace( + '"protocol": "io.github.cigaleapp.arthropods.example.light"', + '"protocol": "io.github.cigaleapp.arthropods.example.wrong"' + ); + zip.file('analysis.json', modifiedContent); + const newZipData = await zip.generateAsync({ type: 'nodebuffer' }); + await writeFile(zipPath, newZipData); +});
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #925
TODO:
fix label-less metadata not shown in picker (see crop in groupby metadata submenus)crop was not set as groupable in protocol v52Later:
set location tolerance in meters/kilometers: https://stackoverflow.com/questions/71208329/gps-coordinates-how-to-round-to-a-define-number-of-kilometersdo this when we have geocoding (so probably when maps and itineraries are added), otherwise its kinda hard to show a meaningful group label anywaysmaybe notalso, because it gotta be useful for at least dates