Support native Git (experimental) (#338)
This commit is contained in:
@@ -20,9 +20,7 @@ test("delete app", async ({ po }) => {
|
||||
await po.page.getByRole("button", { name: "Delete App" }).click();
|
||||
|
||||
// Make sure the app is deleted
|
||||
await expect(async () => {
|
||||
expect(await po.getCurrentAppName()).toBe("(no app selected)");
|
||||
}).toPass();
|
||||
await po.isCurrentAppNameNone();
|
||||
expect(fs.existsSync(appPath)).toBe(false);
|
||||
expect(po.getAppListItem({ appName })).not.toBeVisible();
|
||||
});
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
avoid AI_RULES auto-prompt
|
||||
1
e2e-tests/fixtures/import-app/version-integrity/a.txt
Normal file
1
e2e-tests/fixtures/import-app/version-integrity/a.txt
Normal file
@@ -0,0 +1 @@
|
||||
a
|
||||
1
e2e-tests/fixtures/import-app/version-integrity/b.txt
Normal file
1
e2e-tests/fixtures/import-app/version-integrity/b.txt
Normal file
@@ -0,0 +1 @@
|
||||
b
|
||||
@@ -0,0 +1 @@
|
||||
dir/c.txt
|
||||
@@ -0,0 +1 @@
|
||||
this file should be deleted
|
||||
@@ -0,0 +1 @@
|
||||
before-edit
|
||||
10
e2e-tests/fixtures/version-integrity-add-edit-delete.md
Normal file
10
e2e-tests/fixtures/version-integrity-add-edit-delete.md
Normal file
@@ -0,0 +1,10 @@
|
||||
Deleting a file
|
||||
|
||||
<dyad-delete path="to-be-deleted.txt"></dyad-delete>
|
||||
<dyad-write path="new-file.js" description="new file">
|
||||
new-file
|
||||
end of new-file
|
||||
</dyad-write>
|
||||
<dyad-write path="to-be-edited.txt" description="editing file">
|
||||
after-edit
|
||||
</dyad-write>
|
||||
2
e2e-tests/fixtures/version-integrity-move-file.md
Normal file
2
e2e-tests/fixtures/version-integrity-move-file.md
Normal file
@@ -0,0 +1,2 @@
|
||||
Moving a file
|
||||
<dyad-rename from="dir/c.txt" to="new-dir/d.txt"></dyad-rename>
|
||||
@@ -15,7 +15,7 @@ export const Timeout = {
|
||||
MEDIUM: os.platform() === "win32" ? 30_000 : 15_000,
|
||||
};
|
||||
|
||||
class PageObject {
|
||||
export class PageObject {
|
||||
private userDataDir: string;
|
||||
|
||||
constructor(
|
||||
@@ -26,11 +26,17 @@ class PageObject {
|
||||
this.userDataDir = userDataDir;
|
||||
}
|
||||
|
||||
async setUp({ autoApprove = false }: { autoApprove?: boolean } = {}) {
|
||||
async setUp({
|
||||
autoApprove = false,
|
||||
nativeGit = false,
|
||||
}: { autoApprove?: boolean; nativeGit?: boolean } = {}) {
|
||||
await this.goToSettingsTab();
|
||||
if (autoApprove) {
|
||||
await this.toggleAutoApprove();
|
||||
}
|
||||
if (nativeGit) {
|
||||
await this.toggleNativeGit();
|
||||
}
|
||||
await this.setUpTestProvider();
|
||||
await this.setUpTestModel();
|
||||
|
||||
@@ -67,6 +73,37 @@ class PageObject {
|
||||
// await page.getByRole('button', { name: 'Select Folder' }).press('Escape');
|
||||
}
|
||||
|
||||
async snapshotAppFiles({ name }: { name?: string } = {}) {
|
||||
const appPath = await this.getCurrentAppPath();
|
||||
if (!appPath || !fs.existsSync(appPath)) {
|
||||
throw new Error(`App path does not exist: ${appPath}`);
|
||||
}
|
||||
|
||||
await expect(() => {
|
||||
const filesData = generateAppFilesSnapshotData(appPath, appPath, [
|
||||
".git",
|
||||
"node_modules",
|
||||
// Avoid snapshotting lock files because they are getting generated
|
||||
// automatically and cause noise, and not super important anyways.
|
||||
"package-lock.json",
|
||||
"pnpm-lock.yaml",
|
||||
]);
|
||||
|
||||
// Sort by relative path to ensure deterministic output
|
||||
filesData.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
|
||||
|
||||
const snapshotContent = filesData
|
||||
.map((file) => `=== ${file.relativePath} ===\n${file.content}`)
|
||||
.join("\n\n");
|
||||
|
||||
if (name) {
|
||||
expect(snapshotContent).toMatchSnapshot(name);
|
||||
} else {
|
||||
expect(snapshotContent).toMatchSnapshot();
|
||||
}
|
||||
}).toPass();
|
||||
}
|
||||
|
||||
async snapshotMessages({
|
||||
replaceDumpPath = false,
|
||||
}: { replaceDumpPath?: boolean } = {}) {
|
||||
@@ -134,9 +171,10 @@ class PageObject {
|
||||
return this.page.getByTestId("preview-iframe-element");
|
||||
}
|
||||
|
||||
async snapshotPreview() {
|
||||
async snapshotPreview({ name }: { name?: string } = {}) {
|
||||
const iframe = this.getPreviewIframeElement();
|
||||
await expect(iframe.contentFrame().locator("body")).toMatchAriaSnapshot({
|
||||
name,
|
||||
timeout: Timeout.LONG,
|
||||
});
|
||||
}
|
||||
@@ -299,7 +337,21 @@ class PageObject {
|
||||
return this.page.getByTestId(`app-list-item-${appName}`);
|
||||
}
|
||||
|
||||
async isCurrentAppNameNone() {
|
||||
await expect(async () => {
|
||||
await expect(this.getTitleBarAppNameButton()).toContainText(
|
||||
"no app selected",
|
||||
);
|
||||
}).toPass();
|
||||
}
|
||||
|
||||
async getCurrentAppName() {
|
||||
// Make sure to wait for the app to be set to avoid a race condition.
|
||||
await expect(async () => {
|
||||
await expect(this.getTitleBarAppNameButton()).not.toContainText(
|
||||
"no app selected",
|
||||
);
|
||||
}).toPass();
|
||||
return (await this.getTitleBarAppNameButton().textContent())?.replace(
|
||||
"App: ",
|
||||
"",
|
||||
@@ -338,6 +390,10 @@ class PageObject {
|
||||
await this.page.getByRole("switch", { name: "Auto-approve" }).click();
|
||||
}
|
||||
|
||||
async toggleNativeGit() {
|
||||
await this.page.getByRole("switch", { name: "Enable Native Git" }).click();
|
||||
}
|
||||
|
||||
async snapshotSettings() {
|
||||
const settings = path.join(this.userDataDir, "user-settings.json");
|
||||
const settingsContent = fs.readFileSync(settings, "utf-8");
|
||||
@@ -588,3 +644,48 @@ function prettifyDump(
|
||||
})
|
||||
.join("\n\n");
|
||||
}
|
||||
|
||||
interface FileSnapshotData {
|
||||
relativePath: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
function generateAppFilesSnapshotData(
|
||||
currentPath: string,
|
||||
basePath: string,
|
||||
ignorePatterns: string[],
|
||||
): FileSnapshotData[] {
|
||||
const entries = fs.readdirSync(currentPath, { withFileTypes: true });
|
||||
let files: FileSnapshotData[] = [];
|
||||
|
||||
// Sort entries for deterministic order
|
||||
entries.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
for (const entry of entries) {
|
||||
const entryPath = path.join(currentPath, entry.name);
|
||||
if (ignorePatterns.includes(entry.name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
files = files.concat(
|
||||
generateAppFilesSnapshotData(entryPath, basePath, ignorePatterns),
|
||||
);
|
||||
} else if (entry.isFile()) {
|
||||
const relativePath = path.relative(basePath, entryPath);
|
||||
try {
|
||||
const content = fs.readFileSync(entryPath, "utf-8");
|
||||
files.push({ relativePath, content });
|
||||
} catch (error) {
|
||||
// Could be a binary file or permission issue, log and add a placeholder
|
||||
const e = error as Error;
|
||||
console.warn(`Could not read file ${entryPath}: ${e.message}`);
|
||||
files.push({
|
||||
relativePath,
|
||||
content: `[Error reading file: ${e.message}]`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
7
e2e-tests/snapshots/switch_versions.spec.ts_v1
Normal file
7
e2e-tests/snapshots/switch_versions.spec.ts_v1
Normal file
@@ -0,0 +1,7 @@
|
||||
- region "Notifications (F8)":
|
||||
- list
|
||||
- region "Notifications alt+T"
|
||||
- heading "Welcome to Your Blank App" [level=1]
|
||||
- paragraph: Start building your amazing project here!
|
||||
- link "Made with Dyad":
|
||||
- /url: https://www.dyad.sh/
|
||||
4
e2e-tests/snapshots/switch_versions.spec.ts_v2
Normal file
4
e2e-tests/snapshots/switch_versions.spec.ts_v2
Normal file
@@ -0,0 +1,4 @@
|
||||
- region "Notifications (F8)":
|
||||
- list
|
||||
- region "Notifications alt+T"
|
||||
- text: Testing:write-index!
|
||||
18
e2e-tests/snapshots/version_integrity.spec.ts_v1
Normal file
18
e2e-tests/snapshots/version_integrity.spec.ts_v1
Normal file
@@ -0,0 +1,18 @@
|
||||
=== a.txt ===
|
||||
a
|
||||
|
||||
=== AI_RULES.md ===
|
||||
avoid AI_RULES auto-prompt
|
||||
|
||||
|
||||
=== b.txt ===
|
||||
b
|
||||
|
||||
=== dir/c.txt ===
|
||||
dir/c.txt
|
||||
|
||||
=== to-be-deleted.txt ===
|
||||
this file should be deleted
|
||||
|
||||
=== to-be-edited.txt ===
|
||||
before-edit
|
||||
19
e2e-tests/snapshots/version_integrity.spec.ts_v2
Normal file
19
e2e-tests/snapshots/version_integrity.spec.ts_v2
Normal file
@@ -0,0 +1,19 @@
|
||||
=== a.txt ===
|
||||
a
|
||||
|
||||
=== AI_RULES.md ===
|
||||
avoid AI_RULES auto-prompt
|
||||
|
||||
|
||||
=== b.txt ===
|
||||
b
|
||||
|
||||
=== dir/c.txt ===
|
||||
dir/c.txt
|
||||
|
||||
=== new-file.js ===
|
||||
new-file
|
||||
end of new-file
|
||||
|
||||
=== to-be-edited.txt ===
|
||||
after-edit
|
||||
19
e2e-tests/snapshots/version_integrity.spec.ts_v3
Normal file
19
e2e-tests/snapshots/version_integrity.spec.ts_v3
Normal file
@@ -0,0 +1,19 @@
|
||||
=== a.txt ===
|
||||
a
|
||||
|
||||
=== AI_RULES.md ===
|
||||
avoid AI_RULES auto-prompt
|
||||
|
||||
|
||||
=== b.txt ===
|
||||
b
|
||||
|
||||
=== new-dir/d.txt ===
|
||||
dir/c.txt
|
||||
|
||||
=== new-file.js ===
|
||||
new-file
|
||||
end of new-file
|
||||
|
||||
=== to-be-edited.txt ===
|
||||
after-edit
|
||||
@@ -1,22 +1,30 @@
|
||||
import { testSkipIfWindows } from "./helpers/test_helper";
|
||||
import { PageObject, testSkipIfWindows } from "./helpers/test_helper";
|
||||
import { expect } from "@playwright/test";
|
||||
|
||||
testSkipIfWindows("switch versions", async ({ po }) => {
|
||||
await po.setUp({ autoApprove: true });
|
||||
const runSwitchVersionTest = async (po: PageObject, nativeGit: boolean) => {
|
||||
await po.setUp({ autoApprove: true, nativeGit });
|
||||
await po.sendPrompt("tc=write-index");
|
||||
|
||||
await po.snapshotPreview();
|
||||
await po.snapshotPreview({ name: `v2` });
|
||||
|
||||
expect(
|
||||
await po.page.getByRole("button", { name: "Version" }).textContent(),
|
||||
).toBe("Version 2");
|
||||
await po.page.getByRole("button", { name: "Version" }).click();
|
||||
await po.page.getByText("Init Dyad app Undo").click();
|
||||
await po.snapshotPreview();
|
||||
await po.snapshotPreview({ name: `v1` });
|
||||
|
||||
await po.page.getByRole("button", { name: "Undo to latest version" }).click();
|
||||
// Should be same as the previous snapshot, but just to be sure.
|
||||
await po.snapshotPreview();
|
||||
await po.snapshotPreview({ name: `v1` });
|
||||
|
||||
await expect(po.page.getByText("Version 3")).toBeVisible();
|
||||
};
|
||||
|
||||
testSkipIfWindows("switch versions", async ({ po }) => {
|
||||
await runSwitchVersionTest(po, false);
|
||||
});
|
||||
|
||||
testSkipIfWindows("switch versions with native git", async ({ po }) => {
|
||||
await runSwitchVersionTest(po, true);
|
||||
});
|
||||
|
||||
@@ -15,6 +15,6 @@ test("create next.js app", async ({ po }) => {
|
||||
await po.clickRestart();
|
||||
|
||||
// This can be pretty slow because it's waiting for the app to build.
|
||||
await expect(po.getPreviewIframeElement()).toBeVisible({ timeout: 50_000 });
|
||||
await expect(po.getPreviewIframeElement()).toBeVisible({ timeout: 100_000 });
|
||||
await po.snapshotPreview();
|
||||
});
|
||||
|
||||
51
e2e-tests/version_integrity.spec.ts
Normal file
51
e2e-tests/version_integrity.spec.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { PageObject, testSkipIfWindows } from "./helpers/test_helper";
|
||||
|
||||
import * as eph from "electron-playwright-helpers";
|
||||
import path from "node:path";
|
||||
|
||||
const runVersionIntegrityTest = async (po: PageObject, nativeGit: boolean) => {
|
||||
await po.setUp({ autoApprove: true, nativeGit });
|
||||
|
||||
// Importing a simple app with a few files.
|
||||
await po.page.getByRole("button", { name: "Import App" }).click();
|
||||
await eph.stubDialog(po.electronApp, "showOpenDialog", {
|
||||
filePaths: [
|
||||
path.join(__dirname, "fixtures", "import-app", "version-integrity"),
|
||||
],
|
||||
});
|
||||
|
||||
await po.page.getByRole("button", { name: "Select Folder" }).click();
|
||||
await po.page.getByRole("textbox", { name: "Enter new app name" }).click();
|
||||
await po.page
|
||||
.getByRole("textbox", { name: "Enter new app name" })
|
||||
.fill("version-integrity-app");
|
||||
await po.page.getByRole("button", { name: "Import" }).click();
|
||||
|
||||
// Initial snapshot
|
||||
await po.snapshotAppFiles({ name: "v1" });
|
||||
|
||||
// Add a file and delete a file
|
||||
await po.sendPrompt("tc=version-integrity-add-edit-delete");
|
||||
await po.snapshotAppFiles({ name: "v2" });
|
||||
|
||||
// Move a file
|
||||
await po.sendPrompt("tc=version-integrity-move-file");
|
||||
await po.snapshotAppFiles({ name: "v3" });
|
||||
|
||||
// Open version pane
|
||||
await po.page.getByRole("button", { name: "Version 3" }).click();
|
||||
await po.page.getByText("Init Dyad app Undo").click();
|
||||
await po.snapshotAppFiles({ name: "v1" });
|
||||
|
||||
await po.page.getByRole("button", { name: "Undo to latest version" }).click();
|
||||
// Should be same as the previous snapshot, but just to be sure.
|
||||
await po.snapshotAppFiles({ name: "v1" });
|
||||
};
|
||||
|
||||
testSkipIfWindows("version integrity (git isomorphic)", async ({ po }) => {
|
||||
await runVersionIntegrityTest(po, false);
|
||||
});
|
||||
|
||||
testSkipIfWindows("version integrity (git native)", async ({ po }) => {
|
||||
await runVersionIntegrityTest(po, true);
|
||||
});
|
||||
Reference in New Issue
Block a user