feat: implement fuzzy search and replace functionality with Levenshtein distance
- Added `applySearchReplace` function to handle search and replace operations with fuzzy matching capabilities. - Introduced tests for various scenarios including fuzzy matching with typos, exact matches, and handling whitespace differences. - Created a parser for search/replace blocks to facilitate the new functionality. - Updated prompts for search-replace operations to clarify usage and examples. - Added utility functions for text normalization and language detection based on file extensions. - Implemented a minimal stdio MCP server for local testing with tools for adding numbers and printing environment variables.
This commit is contained in:
@@ -71,16 +71,25 @@ class ProModesDialog {
|
||||
public close: () => Promise<void>,
|
||||
) {}
|
||||
|
||||
async setSmartContextMode(mode: "balanced" | "off" | "conservative") {
|
||||
async setSmartContextMode(mode: "balanced" | "off" | "deep") {
|
||||
await this.page
|
||||
.getByTestId("smart-context-selector")
|
||||
.getByRole("button", {
|
||||
name: mode.charAt(0).toUpperCase() + mode.slice(1),
|
||||
})
|
||||
.click();
|
||||
}
|
||||
|
||||
async toggleTurboEdits() {
|
||||
await this.page.getByRole("switch", { name: "Turbo Edits" }).click();
|
||||
async setTurboEditsMode(mode: "off" | "classic" | "search-replace") {
|
||||
await this.page
|
||||
.getByTestId("turbo-edits-selector")
|
||||
.getByRole("button", {
|
||||
name:
|
||||
mode === "search-replace"
|
||||
? "Search & replace"
|
||||
: mode.charAt(0).toUpperCase() + mode.slice(1),
|
||||
})
|
||||
.click();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -330,7 +339,7 @@ export class PageObject {
|
||||
await this.page.getByRole("button", { name: "Import" }).click();
|
||||
}
|
||||
|
||||
async selectChatMode(mode: "build" | "ask") {
|
||||
async selectChatMode(mode: "build" | "ask" | "agent") {
|
||||
await this.page.getByTestId("chat-mode-selector").click();
|
||||
await this.page.getByRole("option", { name: mode }).click();
|
||||
}
|
||||
@@ -362,7 +371,7 @@ export class PageObject {
|
||||
await expect(this.page.getByRole("dialog")).toMatchAriaSnapshot();
|
||||
}
|
||||
|
||||
async snapshotAppFiles({ name }: { name: string }) {
|
||||
async snapshotAppFiles({ name, files }: { name: string; files?: string[] }) {
|
||||
const currentAppName = await this.getCurrentAppName();
|
||||
if (!currentAppName) {
|
||||
throw new Error("No app selected");
|
||||
@@ -374,10 +383,17 @@ export class PageObject {
|
||||
}
|
||||
|
||||
await expect(() => {
|
||||
const filesData = generateAppFilesSnapshotData(appPath, appPath);
|
||||
let filesData = generateAppFilesSnapshotData(appPath, appPath);
|
||||
|
||||
// Sort by relative path to ensure deterministic output
|
||||
filesData.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
|
||||
if (files) {
|
||||
filesData = filesData.filter((file) =>
|
||||
files.some(
|
||||
(f) => normalizePath(f) === normalizePath(file.relativePath),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const snapshotContent = filesData
|
||||
.map(
|
||||
@@ -400,7 +416,8 @@ export class PageObject {
|
||||
|
||||
async snapshotMessages({
|
||||
replaceDumpPath = false,
|
||||
}: { replaceDumpPath?: boolean } = {}) {
|
||||
timeout,
|
||||
}: { replaceDumpPath?: boolean; timeout?: number } = {}) {
|
||||
if (replaceDumpPath) {
|
||||
// Update page so that "[[dyad-dump-path=*]]" is replaced with a placeholder path
|
||||
// which is stable across runs.
|
||||
@@ -417,7 +434,9 @@ export class PageObject {
|
||||
);
|
||||
});
|
||||
}
|
||||
await expect(this.page.getByTestId("messages-list")).toMatchAriaSnapshot();
|
||||
await expect(this.page.getByTestId("messages-list")).toMatchAriaSnapshot({
|
||||
timeout,
|
||||
});
|
||||
}
|
||||
|
||||
async approveProposal() {
|
||||
@@ -431,15 +450,48 @@ export class PageObject {
|
||||
async clickRestart() {
|
||||
await this.page.getByRole("button", { name: "Restart" }).click();
|
||||
}
|
||||
////////////////////////////////
|
||||
// Inline code editor
|
||||
////////////////////////////////
|
||||
async clickEditButton() {
|
||||
await this.page.locator('button:has-text("Edit")').first().click();
|
||||
}
|
||||
|
||||
async editFileContent(content: string) {
|
||||
const editor = this.page.locator(".monaco-editor textarea").first();
|
||||
await editor.focus();
|
||||
await editor.press("Home");
|
||||
await editor.type(content);
|
||||
}
|
||||
|
||||
async saveFile() {
|
||||
await this.page.locator('[data-testid="save-file-button"]').click();
|
||||
}
|
||||
|
||||
async cancelEdit() {
|
||||
await this.page.locator('button:has-text("Cancel")').first().click();
|
||||
}
|
||||
|
||||
////////////////////////////////
|
||||
// Preview panel
|
||||
////////////////////////////////
|
||||
|
||||
async selectPreviewMode(mode: "code" | "problems" | "preview" | "configure") {
|
||||
async selectPreviewMode(
|
||||
mode: "code" | "problems" | "preview" | "configure" | "security",
|
||||
) {
|
||||
await this.page.getByTestId(`${mode}-mode-button`).click();
|
||||
}
|
||||
|
||||
async clickChatActivityButton() {
|
||||
await this.page.getByTestId("chat-activity-button").click();
|
||||
}
|
||||
|
||||
async snapshotChatActivityList() {
|
||||
await expect(
|
||||
this.page.getByTestId("chat-activity-list"),
|
||||
).toMatchAriaSnapshot();
|
||||
}
|
||||
|
||||
async clickRecheckProblems() {
|
||||
await this.page.getByTestId("recheck-button").click();
|
||||
}
|
||||
@@ -470,8 +522,15 @@ export class PageObject {
|
||||
.click({ timeout: Timeout.EXTRA_LONG });
|
||||
}
|
||||
|
||||
async clickDeselectComponent() {
|
||||
await this.page.getByRole("button", { name: "Deselect component" }).click();
|
||||
async clickDeselectComponent(options?: { index?: number }) {
|
||||
const buttons = this.page.getByRole("button", {
|
||||
name: "Deselect component",
|
||||
});
|
||||
if (options?.index !== undefined) {
|
||||
await buttons.nth(options.index).click();
|
||||
} else {
|
||||
await buttons.first().click();
|
||||
}
|
||||
}
|
||||
|
||||
async clickPreviewMoreOptions() {
|
||||
@@ -530,12 +589,12 @@ export class PageObject {
|
||||
await expect(this.getChatInputContainer()).toMatchAriaSnapshot();
|
||||
}
|
||||
|
||||
getSelectedComponentDisplay() {
|
||||
getSelectedComponentsDisplay() {
|
||||
return this.page.getByTestId("selected-component-display");
|
||||
}
|
||||
|
||||
async snapshotSelectedComponentDisplay() {
|
||||
await expect(this.getSelectedComponentDisplay()).toMatchAriaSnapshot();
|
||||
async snapshotSelectedComponentsDisplay() {
|
||||
await expect(this.getSelectedComponentsDisplay()).toMatchAriaSnapshot();
|
||||
}
|
||||
|
||||
async snapshotPreview({ name }: { name?: string } = {}) {
|
||||
@@ -546,6 +605,12 @@ export class PageObject {
|
||||
});
|
||||
}
|
||||
|
||||
async snapshotSecurityFindingsTable() {
|
||||
await expect(
|
||||
this.page.getByTestId("security-findings-table"),
|
||||
).toMatchAriaSnapshot();
|
||||
}
|
||||
|
||||
async snapshotServerDump(
|
||||
type: "all-messages" | "last-message" | "request" = "all-messages",
|
||||
{ name = "", dumpIndex = -1 }: { name?: string; dumpIndex?: number } = {},
|
||||
@@ -648,7 +713,7 @@ export class PageObject {
|
||||
|
||||
getChatInput() {
|
||||
return this.page.locator(
|
||||
'[data-lexical-editor="true"][aria-placeholder="Ask Dyad to build..."]',
|
||||
'[data-lexical-editor="true"][aria-placeholder^="Ask Dyad to build"]',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -664,16 +729,21 @@ export class PageObject {
|
||||
await this.page.getByRole("button", { name: "Back" }).click();
|
||||
}
|
||||
|
||||
async sendPrompt(prompt: string) {
|
||||
async sendPrompt(
|
||||
prompt: string,
|
||||
{ skipWaitForCompletion = false }: { skipWaitForCompletion?: boolean } = {},
|
||||
) {
|
||||
await this.getChatInput().click();
|
||||
await this.getChatInput().fill(prompt);
|
||||
await this.page.getByRole("button", { name: "Send message" }).click();
|
||||
await this.waitForChatCompletion();
|
||||
if (!skipWaitForCompletion) {
|
||||
await this.waitForChatCompletion();
|
||||
}
|
||||
}
|
||||
|
||||
async selectModel({ provider, model }: { provider: string; model: string }) {
|
||||
await this.page.getByRole("button", { name: "Model: Auto" }).click();
|
||||
await this.page.getByText(provider).click();
|
||||
await this.page.getByText(provider, { exact: true }).click();
|
||||
await this.page.getByText(model, { exact: true }).click();
|
||||
}
|
||||
|
||||
@@ -701,6 +771,24 @@ export class PageObject {
|
||||
.click();
|
||||
}
|
||||
|
||||
async selectTestAzureModel() {
|
||||
await this.page.getByRole("button", { name: "Model: Auto" }).click();
|
||||
await this.page.getByText("Other AI providers").click();
|
||||
await this.page.getByText("Azure OpenAI", { exact: true }).click();
|
||||
await this.page.getByText("GPT-5", { exact: true }).click();
|
||||
}
|
||||
|
||||
async setUpAzure({ autoApprove = false }: { autoApprove?: boolean } = {}) {
|
||||
await this.githubConnector.clearPushEvents();
|
||||
await this.goToSettingsTab();
|
||||
if (autoApprove) {
|
||||
await this.toggleAutoApprove();
|
||||
}
|
||||
// Azure should already be configured via environment variables
|
||||
// so we don't need additional setup steps like setUpDyadProvider
|
||||
await this.goToAppsTab();
|
||||
}
|
||||
|
||||
async setUpTestProvider() {
|
||||
await this.page.getByText("Add custom providerConnect to").click();
|
||||
// Fill out provider dialog
|
||||
@@ -719,9 +807,7 @@ export class PageObject {
|
||||
}
|
||||
|
||||
async setUpTestModel() {
|
||||
await this.page
|
||||
.getByRole("heading", { name: "test-provider Needs Setup" })
|
||||
.click();
|
||||
await this.page.getByRole("heading", { name: "test-provider" }).click();
|
||||
await this.page.getByRole("button", { name: "Add Custom Model" }).click();
|
||||
await this.page
|
||||
.getByRole("textbox", { name: "Model ID*" })
|
||||
@@ -984,6 +1070,7 @@ export class PageObject {
|
||||
|
||||
interface ElectronConfig {
|
||||
preLaunchHook?: ({ userDataDir }: { userDataDir: string }) => Promise<void>;
|
||||
showSetupScreen?: boolean;
|
||||
}
|
||||
|
||||
// From https://github.com/microsoft/playwright/issues/8208#issuecomment-1435475930
|
||||
@@ -1046,8 +1133,10 @@ export const test = base.extend<{
|
||||
process.env.DYAD_ENGINE_URL = "http://localhost:3500/engine/v1";
|
||||
process.env.DYAD_GATEWAY_URL = "http://localhost:3500/gateway/v1";
|
||||
process.env.E2E_TEST_BUILD = "true";
|
||||
// This is just a hack to avoid the AI setup screen.
|
||||
process.env.OPENAI_API_KEY = "sk-test";
|
||||
if (!electronConfig.showSetupScreen) {
|
||||
// This is just a hack to avoid the AI setup screen.
|
||||
process.env.OPENAI_API_KEY = "sk-test";
|
||||
}
|
||||
const baseTmpDir = os.tmpdir();
|
||||
const userDataDir = path.join(baseTmpDir, `dyad-e2e-tests-${Date.now()}`);
|
||||
if (electronConfig.preLaunchHook) {
|
||||
@@ -1131,6 +1220,17 @@ export function testWithConfig(config: ElectronConfig) {
|
||||
});
|
||||
}
|
||||
|
||||
export function testWithConfigSkipIfWindows(config: ElectronConfig) {
|
||||
if (os.platform() === "win32") {
|
||||
return test.skip;
|
||||
}
|
||||
return test.extend({
|
||||
electronConfig: async ({}, use) => {
|
||||
await use(config);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Wrapper that skips tests on Windows platform
|
||||
export const testSkipIfWindows = os.platform() === "win32" ? test.skip : test;
|
||||
|
||||
@@ -1163,3 +1263,7 @@ function prettifyDump(
|
||||
})
|
||||
.join("\n\n");
|
||||
}
|
||||
|
||||
function normalizePath(path: string): string {
|
||||
return path.replace(/\\/g, "/");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user