diff --git a/datasets/populate b/datasets/populate index 17a7d482c..41156afc0 100755 --- a/datasets/populate +++ b/datasets/populate @@ -10,10 +10,8 @@ curl 'https://storage.googleapis.com/deai-313515.appspot.com/example_training_da # lungs ultrasound mkdir -p lus_covid -curl 'https://drive.switch.ch/index.php/s/zM5ZrUWK3taaIly/download' > archive.zip -ln -fs lus_covid DeAI-testimages # redirect top level dir -unzip -u archive.zip -rm archive.zip DeAI-testimages +curl 'https://storage.googleapis.com/deai-313515.appspot.com/lus_covid.tar.gz'| + tar -xz # wikitext mkdir -p wikitext diff --git a/webapp/cypress/e2e/datasetInput.cy.ts b/webapp/cypress/e2e/datasetInput.cy.ts index 50aeafbe6..43b2da26e 100644 --- a/webapp/cypress/e2e/datasetInput.cy.ts +++ b/webapp/cypress/e2e/datasetInput.cy.ts @@ -3,6 +3,55 @@ import { basicTask, setupServerWith } from "../support/e2e"; // TODO move to components testing // upstream doesn't yet allow that vuejs/test-utils#2468 +function droppedFolder(name: string, filenames: string[]): unknown { + const fileEntry = (filename: string) => ({ + isFile: true, + isDirectory: false, + name: filename, + file: (onSuccess: (file: File) => void) => + onSuccess( + new File([], filename, { + type: filename.endsWith(".png") ? "image/png" : "", + }), + ), + }); + + let read = false; + const directoryEntry = { + isFile: false, + isDirectory: true, + name, + createReader: () => ({ + readEntries: (onSuccess: (entries: unknown[]) => void) => { + onSuccess(read ? [] : filenames.map(fileEntry)); + read = true; + }, + }), + }; + + return { + items: [{ webkitGetAsEntry: () => directoryEntry }], + files: [], + dropEffect: "none", + }; +} + +function droppedFile(name: string, type: string): unknown { + const fileEntry = { + isFile: true, + isDirectory: false, + name, + file: (onSuccess: (file: File) => void) => + onSuccess(new File([], name, { type })), + }; + + return { + items: [{ webkitGetAsEntry: () => fileEntry }], + files: [], + dropEffect: "none", + }; +} + function goToDatasetInputStep() { cy.visit("/list"); cy.get(".driver-popover-close-btn").click(); @@ -50,6 +99,55 @@ describe("image dataset input by group", () => { cy.contains("Number of selected files: 3").should("exist"); }); + + it("allows to drop a folder of images", () => { + setupServerWith( + basicTask("image", { + LABEL_LIST: ["label"], + IMAGE_H: 100, + IMAGE_W: 100, + }), + ); + + goToDatasetInputStep(); + cy.get("button").contains("group").click(); + cy.contains("Drop images or a folder here"); + + cy.get('[data-testid="drop-image-area"]') + .first() + .trigger("drop", { + dataTransfer: droppedFolder("COVID+", [ + "first.png", + "second.png", + ".DS_Store", + ]), + }); + + cy.contains("Number of selected files: 2").should("exist"); + cy.contains("Ignored 1 file(s) that aren't images").should("exist"); + }); + + it("rejects a dropped file that isn't an image", () => { + setupServerWith( + basicTask("image", { + LABEL_LIST: ["label"], + IMAGE_H: 100, + IMAGE_W: 100, + }), + ); + + goToDatasetInputStep(); + cy.get("button").contains("group").click(); + + cy.get('[data-testid="drop-image-area"]') + .first() + .trigger("drop", { + dataTransfer: droppedFile("data.xlsx", "application/vnd.ms-excel"), + }); + + cy.contains("Didn't find any images in what you dropped").should("exist"); + cy.contains("Number of selected files").should("not.exist"); + }); }); describe("image dataset input by csv", () => { diff --git a/webapp/cypress/e2e/notFound.cy.ts b/webapp/cypress/e2e/notFound.cy.ts new file mode 100644 index 000000000..7b41ce6e3 --- /dev/null +++ b/webapp/cypress/e2e/notFound.cy.ts @@ -0,0 +1,41 @@ +import { defaultTasks } from "@epfml/discojs"; + +import { setupServerWith } from "../support/e2e.ts"; + +describe("not-found page", () => { + it("is shown for an unknown task", () => { + setupServerWith(defaultTasks.titanic); + + cy.visit("/not-a-task"); + cy.url().should("eq", `${Cypress.config().baseUrl}not-found`); + cy.contains("404"); + }); + + it("can be navigated away from", () => { + setupServerWith(defaultTasks.titanic); + + cy.visit("/not-a-task"); + cy.contains("404"); + + // the component of the unknown task is kept alive, it shouldn't redirect anymore + cy.get('aside a[href="/list"]').click(); + cy.url().should("eq", `${Cypress.config().baseUrl}list`); + }); + + it("is not covered by the tutorial when leaving it", () => { + setupServerWith(defaultTasks.titanic); + + cy.visit("/not-a-task"); + cy.contains("404"); + + // the tutorial starts on the task list and overlays the whole page + cy.get('aside a[href="/list"]').click(); + cy.get(".driver-popover"); + + // going back leaves no overlay swallowing the clicks + cy.go("back"); + cy.get(".driver-popover").should("not.exist"); + cy.get('aside a[href="/create"]').click(); + cy.url().should("eq", `${Cypress.config().baseUrl}create`); + }); +}); diff --git a/webapp/cypress/e2e/tutorial.cy.ts b/webapp/cypress/e2e/tutorial.cy.ts new file mode 100644 index 000000000..751ee3658 --- /dev/null +++ b/webapp/cypress/e2e/tutorial.cy.ts @@ -0,0 +1,147 @@ +import { defaultTasks } from "@epfml/discojs"; + +import { setupServerWith } from "../support/e2e.ts"; + +// The element highlighted by each step of the tutorial, in order. +// `undefined` for the steps that only display a centered popover. +const STEPS = [ + "#tuto-help-bttn", + undefined, + "#llm_task", + "#tuto-create-bttn", + "#tuto-evaluate-bttn", + "#lus_covid", + undefined, + "#tuto-training-bar", + ".tuto-data-desc", + ".tuto-example-data", + "#tuto-group-bttn", + ".group-data-field", + ".tuto-train-dash", + "#train-collab-bttn", + "#train-locally-bttn", + undefined, + "#tuto-evaluate-bttn", + "#tuto-slack-link", +]; + +function expectStep(index: number): void { + cy.get(".driver-popover-progress-text").should( + "have.text", + `Step ${index + 1} of ${STEPS.length}`, + ); + const selector = STEPS[index]; + if (selector === undefined) return; + cy.get(selector) + .should("have.class", "driver-active-element") + // the highlighted element has to be rendered: the elements of another + // training step are in the DOM but hidden, hence have no dimension + .and(($element) => { + expect( + $element[0].getBoundingClientRect().height, + `${selector} is displayed`, + ).to.be.greaterThan(0); + }); +} + +describe("tutorial", () => { + it("is shown on the first visit only", () => { + setupServerWith(defaultTasks.titanic); + + cy.visit("/list"); + cy.get(".driver-popover-close-btn").click(); + + // having been shown is persisted, it shouldn't show up again on a new load + cy.visit("/list"); + cy.contains("button", "participate"); // wait for the page to be loaded + cy.get(".driver-popover").should("not.exist"); + }); + + it("can still be started from the sidebar", () => { + setupServerWith(defaultTasks.titanic); + + cy.visit("/list"); + cy.get(".driver-popover-close-btn").click(); + + cy.get("#tuto-help-bttn").click(); + cy.get(".driver-popover"); + + // the skipped first step isn't reachable by going back + cy.get(".driver-popover-prev-btn").should("be.disabled"); + }); + + it("shows a single popover when started while navigating to the task list", () => { + setupServerWith(defaultTasks.titanic); + + // starting from another page navigates to the task list, whose mounting + // shouldn't start the tutorial a second time + cy.visit("/"); + cy.get("#tuto-help-bttn").click(); + + cy.contains(".driver-popover-title", "Welcome to DISCO!"); + cy.get(".driver-popover").should("have.length", 1); + }); + + it("only closes when clicking on the cross", () => { + setupServerWith(defaultTasks.titanic); + + cy.visit("/list"); + cy.get(".driver-popover"); + + // clicking outside of the popover is most likely a misclick, it shouldn't close + cy.get(".driver-overlay").click({ force: true }); + cy.get(".driver-popover"); + + cy.get(".driver-popover-close-btn").click(); + cy.get(".driver-popover").should("not.exist"); + }); + + it("displays the current step and the total number of steps", () => { + setupServerWith(defaultTasks.titanic); + + cy.visit("/list"); + cy.get(".driver-popover-progress-text").should("contain", "Step 1 of"); + cy.get(".driver-popover-next-btn").click(); + cy.get(".driver-popover-progress-text").should("contain", "Step 2 of"); + + cy.get(".driver-popover-close-btn").click(); + + // the first step is skipped when starting from the sidebar, + // it shouldn't be counted + cy.get("#tuto-help-bttn").click(); + cy.contains(".driver-popover-title", "Welcome to DISCO!"); + cy.get(".driver-popover-progress-text").should("contain", "Step 1 of"); + }); + it("can be navigated back and forth", () => { + setupServerWith(defaultTasks.lusCovid, defaultTasks.wikitext); + + cy.visit("/list"); + + // "previous" is disabled on the first step + cy.get(".driver-popover-prev-btn").should("be.disabled"); + + // go through the whole tutorial + for (let index = 0; index < STEPS.length; index++) { + expectStep(index); + if (index < STEPS.length - 1) cy.get(".driver-popover-next-btn").click(); + } + cy.url().should("match", /\/lus_covid$/); + + // go back to the beginning + for (let index = STEPS.length - 1; index >= 0; index--) { + expectStep(index); + if (index > 0) cy.get(".driver-popover-prev-btn").click(); + } + cy.url().should("match", /\/list$/); + + // and go the the end again + for (let index = 0; index < STEPS.length; index++) { + expectStep(index); + cy.get(".driver-popover-next-btn").click(); + } + + // the last step closes the tutorial and goes back to the task list + cy.get(".driver-popover").should("not.exist"); + cy.url().should("match", /\/list$/); + }); +}); diff --git a/webapp/src/assets/css/tailwind.css b/webapp/src/assets/css/tailwind.css index e66dc68c1..a578db524 100644 --- a/webapp/src/assets/css/tailwind.css +++ b/webapp/src/assets/css/tailwind.css @@ -25,4 +25,9 @@ --color-heading-light: #334155; --color-heading-dark: #fff; + + /* width of a single content card, the page's content column */ + --container-card: 700px; + /* two cards side by side, separated by `cards-gap` at md and above */ + --container-cards-2: calc(2 * 700px + 2rem); } diff --git a/webapp/src/components/dataset_input/DataDescription.vue b/webapp/src/components/dataset_input/DataDescription.vue index 0382548a6..af41e8d73 100644 --- a/webapp/src/components/dataset_input/DataDescription.vue +++ b/webapp/src/components/dataset_input/DataDescription.vue @@ -1,5 +1,5 @@