Skip to content

Commit

Permalink
e2e tests, zero-state for a few views and a few bug fixes
Browse files Browse the repository at this point in the history
e2e tests are for example-app, but kept in the same cypress setup for simpler reuse of shared cypress configuration needed for both cases.
  • Loading branch information
alirezamirian committed May 27, 2024
1 parent f7772ed commit 9a28666
Show file tree
Hide file tree
Showing 92 changed files with 1,757 additions and 282 deletions.
11 changes: 7 additions & 4 deletions .github/workflows/CI.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -100,17 +100,20 @@ jobs:
env:
PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }}
# Without this, we would need to "finalize" percy build explicitly. More info: https://docs.percy.io/docs/parallel-test-suites
PERCY_PARALLEL_TOTAL: 10
PERCY_PARALLEL_TOTAL: 20 # component and e2e, each 10
# Percy's nonce is supposed to be set to run id by default, but it's not. Plus, appending run_number makes it more reliable in case of rerunning
PERCY_PARALLEL_NONCE: ${{ github.run_id }}-${{ github.run_number }}
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
run: yarn run test:cypress --parallel

run: |
yarn run cypress:component --record --parallel
yarn run cypress:e2e --record --parallel
- name: Run Cypress Tests (without percy) ✅
if: ${{ !(env.IS_PR_JUST_MERGED == 'true' || env.IS_PR_WITH_PERCY == 'true') }}
env:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
run: yarn run test:cypress --parallel
run: |
yarn run cypress:component --record --parallel
yarn run cypress:e2e --record --parallel
- uses: actions/upload-artifact@v2
name: Upload Cypress screenshots if tests failed 🤕
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"scripts": {
"type-check": "yarn workspaces foreach run type-check",
"test": "yarn workspaces foreach run test",
"test:cypress": "yarn workspaces foreach run test:cypress",
"cypress:component": "yarn workspaces foreach run cypress:component",
"cypress:e2e": "yarn workspaces foreach run cypress:e2e",
"lint": "yarn run lint:lib",
"lint:lib": "yarn eslint packages/jui",
"format:check": "prettier packages/jui packages/website .github --check",
Expand Down
6 changes: 3 additions & 3 deletions packages/example-app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
} from "@intellij-platform/core";
import { DefaultSuspense } from "./DefaultSuspense";
import { Project } from "./Project/Project";
import { SampleRepoInitializer } from "./SampleRepoInitializer";
import { ProjectInitializer } from "./ProjectInitializer";
import { fs, WaitForFs } from "./fs/fs";
import { exampleAppKeymap } from "./exampleAppKeymap";
import "./jetbrains-mono-font.css";
Expand All @@ -29,7 +29,7 @@ export const App = ({ height }: { height?: CSSProperties["height"] }) => {
// TODO: add an error boundary
<DefaultSuspense>
<WaitForFs>
<SampleRepoInitializer>
<ProjectInitializer>
<KeymapProvider keymap={exampleAppKeymap}>
<RecoilRoot>
<WindowManager>
Expand All @@ -39,7 +39,7 @@ export const App = ({ height }: { height?: CSSProperties["height"] }) => {
</WindowManager>
</RecoilRoot>
</KeymapProvider>
</SampleRepoInitializer>
</ProjectInitializer>
</WaitForFs>
</DefaultSuspense>
);
Expand Down
47 changes: 47 additions & 0 deletions packages/example-app/src/Editor/EditorZeroState.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React from "react";
import {
CommonActionId,
styled,
useAction,
useGetActionShortcut,
} from "@intellij-platform/core";

const StyledContainer = styled.div.attrs({ tabIndex: -1 })`
display: flex;
flex-direction: column;
height: inherit;
justify-content: center;
padding: 0 25%;
font-size: 1rem;
outline: none;
`;

const StyledLine = styled.div`
display: flex;
gap: 0.5rem;
margin-bottom: 0.5rem;
`;

const StyledShortcut = styled.div`
color: ${({ theme }) => theme.commonColors.linkForegroundEnabled};
`;

export function EditorZeroState() {
return (
<StyledContainer>
<ActionTip actionId={CommonActionId.GO_TO_FILE} />
<ActionTip actionId={CommonActionId.GO_TO_ACTION} />
</StyledContainer>
);
}

function ActionTip({ actionId }: { actionId: string }) {
const getShortcut = useGetActionShortcut();
const action = useAction(actionId);
return (
<StyledLine>
{action?.title}
<StyledShortcut>{getShortcut(actionId)}</StyledShortcut>
</StyledLine>
);
}
235 changes: 121 additions & 114 deletions packages/example-app/src/Editor/FileEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { mergeProps } from "@react-aria/utils";
import { useActivePathsProvider } from "../Project/project.state";
import { notImplemented } from "../Project/notImplemented";
import { useExistingLatestRecoilValue } from "../recoil-utils";
import { EditorZeroState } from "./EditorZeroState";

const editorFullState = selector({
key: "editorState",
Expand All @@ -60,7 +61,7 @@ export const FileEditor = () => {
const hideAllAction = useAction(HIDE_ALL_WINDOWS_ACTION_ID);
const setCursorPositionState = useSetRecoilState(editorCursorPositionState);

const [{ content, editorState, filePath }, loadingState] =
const [{ content, filePath }, loadingState] =
useExistingLatestRecoilValue(editorFullState);

// For functions that are needed in tab action callbacks. Because items are cached and referencing anything
Expand Down Expand Up @@ -107,122 +108,128 @@ export const FileEditor = () => {
},
})}
>
{editorTabs.length > 0 && (
<ContextMenuContainer
renderMenu={() => (
<Menu
// TODO: detect which tab was triggering context menu and handle the action accordingly
// One idea is to use use the data-key attribute, from the closes parent that has one. Maybe a
// CollectionContextMenuContainer component which implements that, while ContextMenuContainer is
// modified to pass the MouseEvent object, in renderMenu.
onAction={notImplemented}
>
<Item key="close">Close</Item>
<Item key="closeOthers">Close Other tabs</Item>
<Item key="closeAll">Close all tabs</Item>
<Item key="closeLeft">Close tabs to the left</Item>
<Item key="closeRight">Close tabs to the right</Item>
</Menu>
)}
>
<EditorTabs
items={editorTabs}
active={active}
selectedKey={filePath}
onSelectionChange={(key) => {
editorStateManager.select(
editorTabs.findIndex((tab) => tab.filePath === key)
);
}}
noBorders
{editorTabs.length > 0 ? (
<>
<ContextMenuContainer
renderMenu={() => (
<Menu
// TODO: detect which tab was triggering context menu and handle the action accordingly
// One idea is to use use the data-key attribute, from the closes parent that has one. Maybe a
// CollectionContextMenuContainer component which implements that, while ContextMenuContainer is
// modified to pass the MouseEvent object, in renderMenu.
onAction={notImplemented}
>
<Item key="close">Close</Item>
<Item key="closeOthers">Close Other tabs</Item>
<Item key="closeAll">Close all tabs</Item>
<Item key="closeLeft">Close tabs to the left</Item>
<Item key="closeRight">Close tabs to the right</Item>
</Menu>
)}
>
{(tab) => {
const filename = path.basename(tab.filePath);
const icon = <PlatformIcon icon={getIconForFile(tab.filePath)} />;
return (
<TabItem
key={tab.filePath}
textValue={filename}
inOverflowMenu={
<MenuItemLayout content={filename} icon={icon} />
}
>
<TooltipTrigger
tooltip={<ActionTooltip actionName={tab.filePath} />}
<EditorTabs
items={editorTabs}
active={active}
selectedKey={filePath}
onSelectionChange={(key) => {
editorStateManager.select(
editorTabs.findIndex((tab) => tab.filePath === key)
);
}}
noBorders
>
{(tab) => {
const filename = path.basename(tab.filePath);
const icon = (
<PlatformIcon icon={getIconForFile(tab.filePath)} />
);
return (
<TabItem
key={tab.filePath}
textValue={filename}
inOverflowMenu={
<MenuItemLayout content={filename} icon={icon} />
}
>
<EditorTabContent
icon={icon}
title={
<FileStatusColor filepath={tab.filePath}>
{filename}
</FileStatusColor>
}
closeButton={
<TooltipTrigger
tooltip={
<ActionTooltip actionName="Close. Alt-Click to Close Others" />
}
>
<TabCloseButton
onPress={(e) => {
if (e.altKey) {
tabActionsRef.current.closeOthersTabs(
editorTabs.indexOf(tab)
);
} else {
tabActionsRef.current.closePath(tab.filePath);
}
}}
/>
</TooltipTrigger>
}
containerProps={{
onDoubleClick: () => {
hideAllAction?.perform();
},
}}
/>
</TooltipTrigger>
</TabItem>
);
}}
</EditorTabs>
</ContextMenuContainer>
)}
{typeof content === "string" ? (
/**
* ## Note
* TLDR: Keeping the editor mounted when filePath changes is intentional and does matter.
*
* Whether the editor component is kept mounted as tabs change or not has nuances that can lead to minor
* focus issues. For example calling editorStateManager.focus() may do nothing if the editor is unmounted due
* to filePath being changed. focusing editor in createFile action is an example of such case. It also affects
* certain focus management code within the FileEditor. For example, focusing the editor on tab changes will
* be necessary if the editor remounts with each file change. Or autofocus behavior of the editor can be
* done on the onMount callback of the Editor component, if it's only mounted once. But the same code leads
* to focus issues if the editor is mounted on active tab changes, when they are not done via the tab UI,
* but as a side effect of another action like opening a file via Project tool window.
*
*/
<Editor
height="100%"
path={filePath}
onMount={(monacoEditor, monaco) => {
monacoEditor.focus();
enableJsx(monaco);
editorRef.current = monacoEditor;
monacoEditor.onDidChangeCursorPosition((e) => {
setCursorPositionState(e.position);
});
monacoEditor.onDidChangeModel(() => {
// TODO: set the editor tab state, and add an atom effect to persist whole editor state across loads
});
}}
onChange={updateContent}
value={content ?? ""}
/>
<TooltipTrigger
tooltip={<ActionTooltip actionName={tab.filePath} />}
>
<EditorTabContent
icon={icon}
title={
<FileStatusColor filepath={tab.filePath}>
{filename}
</FileStatusColor>
}
closeButton={
<TooltipTrigger
tooltip={
<ActionTooltip actionName="Close. Alt-Click to Close Others" />
}
>
<TabCloseButton
onPress={(e) => {
if (e.altKey) {
tabActionsRef.current.closeOthersTabs(
editorTabs.indexOf(tab)
);
} else {
tabActionsRef.current.closePath(tab.filePath);
}
}}
/>
</TooltipTrigger>
}
containerProps={{
onDoubleClick: () => {
hideAllAction?.perform();
},
}}
/>
</TooltipTrigger>
</TabItem>
);
}}
</EditorTabs>
</ContextMenuContainer>
{typeof content === "string" ? (
/**
* ## Note
* TLDR: Keeping the editor mounted when filePath changes is intentional and does matter.
*
* Whether the editor component is kept mounted as tabs change or not has nuances that can lead to minor
* focus issues. For example calling editorStateManager.focus() may do nothing if the editor is unmounted due
* to filePath being changed. focusing editor in createFile action is an example of such case. It also affects
* certain focus management code within the FileEditor. For example, focusing the editor on tab changes will
* be necessary if the editor remounts with each file change. Or autofocus behavior of the editor can be
* done on the onMount callback of the Editor component, if it's only mounted once. But the same code leads
* to focus issues if the editor is mounted on active tab changes, when they are not done via the tab UI,
* but as a side effect of another action like opening a file via Project tool window.
*
*/
<Editor
height="100%"
path={filePath}
onMount={(monacoEditor, monaco) => {
monacoEditor.focus();
enableJsx(monaco);
editorRef.current = monacoEditor;
monacoEditor.onDidChangeCursorPosition((e) => {
setCursorPositionState(e.position);
});
monacoEditor.onDidChangeModel(() => {
// TODO: set the editor tab state, and add an atom effect to persist whole editor state across loads
});
}}
onChange={updateContent}
value={content ?? ""}
/>
) : (
content && "UNSUPPORTED CONTENT"
)}
</>
) : (
content && "UNSUPPORTED CONTENT"
<EditorZeroState></EditorZeroState>
)}
{loadingState === "loading" && <FileEditorLoading />}
</StyledFileEditorContainer>
Expand Down
Loading

0 comments on commit 9a28666

Please sign in to comment.