Skip to content

Commit

Permalink
Sequencer Gallery (#1169)
Browse files Browse the repository at this point in the history
* chore: Sequence Gallery UI

* [sequencer-gallery] chore: Working import demo sequence

* [sequencer-gallery] Export & Expected Demo

* [sequencer-gallery] ui: removed weird yellow and added better comment in example

* chore: formatting

* fix: Eslint

* [sequencer-gallery] chore: using test profile workflow to load example

* [sequencer-gallery] chore: remove "use" prefix as react thinks it's a hook

* [sequencer-gallery] chore: bundle example with app

* [sequencer-gallery] fix: space in sequence name

* chore: formatting

* [sequencer-gallery] chore: using process.resourcesPath

* [sequencer-gallery] chore: adding CI test for Gallery + testing injected min & max

* chore: formatting

* chore(gallery): fix @39bytes comment

* try bumping pnpm version in pacakge.json

---------

Co-authored-by: Jeff Zhang <[email protected]>
Co-authored-by: JeffDotPng <[email protected]>
  • Loading branch information
3 people authored Apr 25, 2024
1 parent f1b82de commit 2f405d4
Show file tree
Hide file tree
Showing 19 changed files with 327 additions and 7 deletions.
2 changes: 2 additions & 0 deletions electron-builder.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ extraResources:
to: "poetry.lock"
- from: "blocks"
to: "blocks"
- from: "examples"
to: "examples"

mac:
icon: ./public/favicon.icns
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"name":"Conditional_Demo","description":"Example","elems":[{"type":"test","id":"ca204090-41bb-4236-97ba-623c4257ef0a","groupId":"41717403-d6ba-49c3-b451-a4a0ae815b2c","path":"test.py::test_will_pass","testName":"test_will_pass","runInParallel":false,"testType":"pytest","status":"pending","error":null,"isSavedToCloud":false,"exportToCloud":true,"createdAt":"2024-04-20T15:57:40.507Z"},{"type":"conditional","id":"5ec59132-f8f5-45be-b0ad-31124a3a6ba0","role":"start","groupId":"0a78f451-a9b6-4fa9-9fe3-16dea93fa46b","conditionalType":"if","condition":" $test_will_pass"},{"type":"test","id":"67014fb0-9a93-433b-8398-df103de3dcc1","groupId":"8a8ecab5-b765-4944-a46c-63111fd59e03","path":"test.py::test_for_example_1","testName":"test_for_example_1","runInParallel":false,"testType":"pytest","status":"pending","error":null,"isSavedToCloud":false,"exportToCloud":true,"createdAt":"2024-04-20T15:57:40.507Z"},{"type":"test","id":"7d6d00ee-f418-49d4-9bcd-5b9cc2e28470","groupId":"e2de5be6-ba8c-4aa8-b6ac-b32b1f30dec1","path":"test.py::test_will_fail","testName":"test_will_fail","runInParallel":false,"testType":"pytest","status":"pending","error":null,"isSavedToCloud":false,"exportToCloud":true,"createdAt":"2024-04-20T15:57:40.507Z"},{"type":"conditional","id":"5ec59132-f8f5-45be-b0ad-31124a3a6ba0","role":"start","groupId":"a45f0b58-b74e-4626-8a0a-2dec3d8e9cfa","conditionalType":"if","condition":" $test_will_fail"},{"type":"test","id":"b4927c15-00e8-4ea4-8788-af395be14775","groupId":"b824ead8-84ad-4de3-88c1-21e4b46343fe","path":"test.py::test_for_example_2","testName":"test_for_example_2","runInParallel":false,"testType":"pytest","status":"pending","error":null,"isSavedToCloud":false,"exportToCloud":true,"createdAt":"2024-04-20T15:57:40.507Z"},{"type":"conditional","id":"d5b1cf12-f50d-4f5a-88d4-b7a9f982a447","role":"between","groupId":"a45f0b58-b74e-4626-8a0a-2dec3d8e9cfa","conditionalType":"else","condition":""},{"type":"test","id":"5ad4da63-40fe-45a2-ab1e-235fd067bde2","groupId":"8f02f062-19f5-448f-83b4-1fcc4e9dc07a","path":"test.py::test_for_example_3","testName":"test_for_example_3","runInParallel":false,"testType":"pytest","status":"pending","error":null,"isSavedToCloud":false,"exportToCloud":true,"createdAt":"2024-04-20T15:57:40.507Z"},{"type":"conditional","id":"c45616fd-cc5a-4469-9bb6-5f3a8ae74bd7","role":"end","groupId":"a45f0b58-b74e-4626-8a0a-2dec3d8e9cfa","conditionalType":"end","condition":""},{"type":"conditional","id":"d5b1cf12-f50d-4f5a-88d4-b7a9f982a447","role":"between","groupId":"0a78f451-a9b6-4fa9-9fe3-16dea93fa46b","conditionalType":"else","condition":""},{"type":"test","id":"01cafc7f-0c21-47b9-8246-305b36a0c23a","groupId":"2e320c36-4b8b-48ec-97af-732e623c3776","path":"test.py::test_for_example_4","testName":"test_for_example_4","runInParallel":false,"testType":"pytest","status":"pending","error":null,"isSavedToCloud":false,"exportToCloud":true,"createdAt":"2024-04-20T15:57:40.507Z"},{"type":"conditional","id":"c45616fd-cc5a-4469-9bb6-5f3a8ae74bd7","role":"end","groupId":"0a78f451-a9b6-4fa9-9fe3-16dea93fa46b","conditionalType":"end","condition":""}],"projectPath":"C:/Users/zzzgu/Documents/flojoy/repo/studio/src/renderer/data/apps/sequencer/conditional/","interpreter":{"type":"flojoy","path":null,"requirementsPath":"flojoy_requirements.txt"}}
Empty file.
28 changes: 28 additions & 0 deletions examples/test-sequencer-conditional-example/test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""
Simple test file to demo how to build conditional tests
- With pytest, all test files should start with 'test_' to be recognized
"""


def test_will_pass():
assert True


def test_will_fail():
assert False


def test_for_example_1():
assert 1 == 1


def test_for_example_2():
assert 2 == 2


def test_for_example_3():
assert 3 == 3


def test_for_example_4():
assert 4 == 4
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"name":"Export_&_Expected_Demo","description":"Consult the Test Step Code!","elems":[{"type":"test","id":"acb47b74-d1dc-4f4b-b3ef-506acb2f53e1","groupId":"f90f758a-a705-4dd0-b3b6-4ae032d22ea3","path":"test.py::test_min_max","testName":"test_min_max","runInParallel":false,"testType":"pytest","status":"pending","error":null,"isSavedToCloud":false,"exportToCloud":true,"createdAt":"2024-04-20T16:13:35.911Z","minValue":5,"maxValue":10,"unit":""},{"type":"test","id":"53bb5746-c720-45c4-8121-9dc469d07927","groupId":"397475ec-c600-4478-8725-b7b8e824d599","path":"test.py::test_min","testName":"test_min","runInParallel":false,"testType":"pytest","status":"pending","error":null,"isSavedToCloud":false,"exportToCloud":true,"createdAt":"2024-04-20T16:13:35.911Z","minValue":5,"unit":""},{"type":"test","id":"55b82abb-37fb-4e32-9379-3609642d01af","groupId":"0377f366-edb7-4a51-97cd-2867bd944cd2","path":"test.py::test_max","testName":"test_max","runInParallel":false,"testType":"pytest","status":"pending","error":null,"isSavedToCloud":false,"exportToCloud":true,"createdAt":"2024-04-20T16:13:35.911Z","maxValue":7,"unit":""},{"type":"test","id":"029a20ad-4c3f-47a5-82aa-2d558eda418a","groupId":"97ba9e80-3133-4dfa-9806-770d47ffb1e4","path":"test.py::test_export_dataframe","testName":"test_export_dataframe","runInParallel":false,"testType":"pytest","status":"pending","error":null,"isSavedToCloud":false,"exportToCloud":true,"createdAt":"2024-04-20T16:13:35.911Z"},{"type":"test","id":"2a75451e-49f6-4810-827e-2b9769d12bc4","groupId":"096274ef-132f-498a-a67a-a279c65b9a8a","path":"test.py::test_export","testName":"test_export","runInParallel":false,"testType":"pytest","status":"pending","error":null,"isSavedToCloud":false,"exportToCloud":true,"createdAt":"2024-04-20T16:13:35.911Z"}],"projectPath":"C:/Users/zzzgu/Documents/flojoy/repo/studio/src/renderer/data/apps/sequencer/expected_exported_values/","interpreter":{"type":"flojoy","path":null,"requirementsPath":"flojoy_requirements.txt"}}
Empty file.
48 changes: 48 additions & 0 deletions examples/test-sequencer-expected-exported-example/test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from flojoy_cloud import test_sequencer
import pandas as pd


def test_min_max():
value = 6.15
test_sequencer.export(value)
assert test_sequencer.is_in_range(value)


def test_min():
value = 6.15
# If not Max value is defined, the value will be checked against the Min value.
test_sequencer.export(value)
assert test_sequencer.is_in_range(value)


def test_max():
value = 6.15
test_sequencer.export(value)

assert test_sequencer.is_in_range(value)
# If multiple assert statements are defined and one of them fails:
# - the rest of the assert statements will not be executed, and the result will
# be reported to the sequencer.
# - the sequencer will report the error, and the test will be marked as failed.
assert 0 < value


def test_export_dataframe():
df = pd.DataFrame({"value": [6.15, 6.15, 6.15]})
# Boolean and DataFrame values will be exported to the Cloud.
test_sequencer.export(df)

assert df is not None


def test_export():
value = 6.15
# Always export as early as possible to avoid missing data.
test_sequencer.export(value)
assert 12 < value # <-- FAIL

# Only the last executed export statement will be exported to the Cloud and
# reported to the sequencer.
test_sequencer.export(20)

assert 0 < value
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -171,5 +171,5 @@
],
"all": true
},
"packageManager": "[email protected].5"
"packageManager": "[email protected].6"
}
69 changes: 69 additions & 0 deletions playwright-test/15_sequences_gallery.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { test, expect, Page, ElectronApplication } from "@playwright/test";
import { _electron as electron } from "playwright";
import {
STARTUP_TIMEOUT,
getExecutablePath,
mockDialogMessage,
standbyStatus,
} from "./utils";
import { Selectors } from "./selectors";

test.describe("Load a demo test sequence", () => {
let window: Page;
let app: ElectronApplication;
test.beforeAll(async () => {
test.setTimeout(STARTUP_TIMEOUT);
const executablePath = getExecutablePath();
app = await electron.launch({ executablePath });
await mockDialogMessage(app);
window = await app.firstWindow();
await expect(
window.locator("code", { hasText: standbyStatus }),
).toBeVisible({ timeout: STARTUP_TIMEOUT });
await window.getByTestId(Selectors.closeWelcomeModalBtn).click();
// Switch to sequencer tab
await window.getByTestId(Selectors.testSequencerTabBtn).click();
});

test.afterAll(async () => {
await app.close();
});

test("Should load and run a sequence", async () => {
await expect(window.getByTestId(Selectors.newDropdown)).toBeEnabled({
timeout: 15000,
});

// Open the sequence gallery
await window.getByTestId(Selectors.newDropdown).click();
await window.getByTestId(Selectors.openSequenceGalleryBtn).click();

// Open a sequence
await window
.getByTestId("test_step_with_expected_and_exported_values")
.nth(1)
.click();

// Expect sequence and tests to be loaded
await expect(
window.locator("div", { hasText: "Export_&_Expected_Demo" }).first(),
).toBeVisible();

// Expect test steps to bey loaded
await expect(
window.locator("div", { hasText: "test_min_max" }).first(),
).toBeVisible();

// Run the sequence
await window.getByTestId(Selectors.runBtn).click();
await window.waitForTimeout(10000);

// Check the status
await expect(window.getByTestId(Selectors.globalStatusBadge)).toContainText(
"FAIL",
);
await expect(window.getByTestId("status-test_min_max")).toContainText(
"PASS",
);
});
});
1 change: 1 addition & 0 deletions playwright-test/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export enum Selectors {
pytestBtn = "pytest-btn",
newDropdown = "new-dropdown",
importTestBtn = "import-test-button",
openSequenceGalleryBtn = "seq-gallery-btn",
globalStatusBadge = "global-status-badge",
newSeqModalNameInput = "new-seq-modal-name-input",
newSeqModalDescInput = "new-seq-modal-desc-input",
Expand Down
2 changes: 2 additions & 0 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,11 +148,13 @@ export default {
openAllFilesInFolder: (
folderPath: string,
allowedExtensions: string[] = ["json"],
relativeToResources: boolean = false,
): Promise<{ filePath: string; fileContent: string }[] | undefined> =>
ipcRenderer.invoke(
API.openAllFilesInFolderPicker,
folderPath,
allowedExtensions,
relativeToResources,
),

getFileContent: (filepath: string): Promise<string> =>
Expand Down
5 changes: 5 additions & 0 deletions src/main/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,12 @@ export const openAllFilesInFolderPicker = (
_,
folderPath: string,
allowedExtensions: string[] = ["json"],
relativeToResources: boolean = false,
): { filePath: string; fileContent: string }[] | undefined => {
// Append the current working directory if the path is relative
if (relativeToResources) {
folderPath = join(process.resourcesPath, folderPath);
}
// Return multiple files or all files with the allowed extensions if a folder is selected
if (!fs.existsSync(folderPath) || !fs.lstatSync(folderPath).isDirectory()) {
return undefined;
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/assets/FlojoyTheme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const flojoySyntaxTheme: SyntaxTheme = {
background: "rgb(var(--color-modal))",
},
"hljs-comment": {
color: "rgb(var(--foreground))",
color: "rgb(var(--color-accent4))",
fontStyle: "italic",
},
"hljs-quote": {
Expand Down
55 changes: 55 additions & 0 deletions src/renderer/hooks/useTestSequencerProject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,61 @@ export const useImportSequences = () => {
return handleImport;
};

export const useImportAllSequencesInFolder = () => {
const manager = usePrepareStateManager();
const { isAdmin } = useWithPermission();

const handleImport = async (path: string, relative: boolean = false) => {
async function importSequences(): Promise<Result<void, Error>> {
// Confirmation if admin
if (!isAdmin()) {
return err(
Error(
"Admin only, Connect to Flojoy Cloud and select a Test Profile",
),
);
}

// Find .tjoy files from the profile
const result = await window.api.openAllFilesInFolder(
path,
["tjoy"],
relative,
);
if (result === undefined) {
return err(Error(`Failed to find the directory ${path}`));
}
if (!result || result.length === 0) {
return err(Error("No .tjoy file found in the selected directory"));
}

// Import them in the sequencer
await Promise.all(
result.map(async (res, idx) => {
const { filePath, fileContent } = res;
const result = await importSequence(
filePath,
fileContent,
manager,
idx !== 0,
);
if (result.isErr()) return err(result.error);
}),
);

return ok(undefined);
}

toastResultPromise(importSequences(), {
loading: `Importing Sequences...`,
success: () => `Sequences imported`,
error: (e) => `${e}`,
});
};

return handleImport;
};

export const useLoadTestProfile = () => {
const manager = usePrepareStateManager();
const { isAdmin } = useWithPermission();
Expand Down
18 changes: 16 additions & 2 deletions src/renderer/routes/test_sequencer_panel/components/DesignBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
HoverCardTrigger,
} from "@/renderer/components/ui/hover-card";
import _ from "lodash";
import { SequencerGalleryModal } from "./modals/SequencerGalleryModal";

export function DesignBar() {
const { setIsImportTestModalOpen, setIsCreateProjectModalOpen } =
Expand Down Expand Up @@ -63,9 +64,14 @@ export function DesignBar() {
}, [elems, sequences, cycleRuns]);

const [displayTotal, setDisplayTotal] = useState(false);
const [isGalleryOpen, setIsGalleryOpen] = useState(false);

return (
<div className=" border-b" style={{ height: ACTIONS_HEIGHT }}>
<SequencerGalleryModal
isGalleryOpen={isGalleryOpen}
setIsGalleryOpen={setIsGalleryOpen}
/>
<div className="py-1" />
<div className="flex">
{isAdmin() && (
Expand Down Expand Up @@ -110,15 +116,23 @@ export function DesignBar() {
<Import size={16} className="mr-2 stroke-muted-foreground" />
Import Sequence
</DropdownMenuItem>
<DropdownMenuItem disabled={true}>
<DropdownMenuItem
onClick={() => setIsGalleryOpen(true)}
data-testid="seq-gallery-btn"
>
<LayoutGrid
size={16}
className="mr-2 stroke-muted-foreground"
/>
Sequence Gallery
Import Example
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<SequencerGalleryModal
isGalleryOpen={isGalleryOpen}
setIsGalleryOpen={setIsGalleryOpen}
/>

<div className="grow" />
</>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,10 @@ export function TestTable() {
header: () => <div className="pl-4 text-center">Status</div>,
cell: ({ row }) => {
return row.original.type === "test" ? (
<div className="my-2 flex justify-center">
<div
className="my-2 flex justify-center"
data-testid={`status-${row.original.testName}`}
>
{typeof mapStatusToDisplay[row.original.status] === "function"
? mapStatusToDisplay[row.original.status](row.original.error)
: mapStatusToDisplay[row.original.status]}
Expand Down
Loading

0 comments on commit 2f405d4

Please sign in to comment.