Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sequencer Gallery #1169

Merged
merged 18 commits into from
Apr 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"}}
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": "pnpm@9.0.5"
"packageManager": "pnpm@9.0.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
Loading