Skip to content
Open
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
87 changes: 87 additions & 0 deletions extensions/vscode/src/utils/quartoProjectHelper.smoke.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,93 @@ Hello project
},
);

// --------------------------------------------------------------------------
// rootDir resolution — verifies that QuartoProjectHelper resolves a relative
// projectDir against rootDir internally, so callers don't need to.
// --------------------------------------------------------------------------
describe("QuartoProjectHelper - rootDir resolution", () => {
test("relative projectDir '.' is resolved to rootDir", () => {
const helper = new QuartoProjectHelper(
"index.qmd",
"index.html",
".",
tmpDir,
);
expect(helper.projectDir).toBe(tmpDir);
});

test("nested relative projectDir is resolved against rootDir", () => {
const subDir = path.join("sub", "project");
const helper = new QuartoProjectHelper(
"index.qmd",
"index.html",
subDir,
tmpDir,
);
expect(helper.projectDir).toBe(path.join(tmpDir, subDir));
});

test("absolute projectDir is unchanged when rootDir is provided", () => {
const absDir = path.join(tmpDir, "already-absolute");
const helper = new QuartoProjectHelper(
"index.qmd",
"index.html",
absDir,
tmpDir,
);
expect(helper.projectDir).toBe(absDir);
});

test("rootDir enables _quarto.yml detection with relative projectDir", async () => {
fs.writeFileSync(
path.join(tmpDir, "_quarto.yml"),
"project:\n type: website\n",
);
// Without rootDir, "." would resolve against cwd (not tmpDir)
const helperWithout = new QuartoProjectHelper(
"index.qmd",
"index.html",
".",
);
// With rootDir, "." resolves to tmpDir where _quarto.yml exists
const helperWith = new QuartoProjectHelper(
"index.qmd",
"index.html",
".",
tmpDir,
);
expect(await helperWithout.isQuartoYmlPresent()).toBe(false);
expect(await helperWith.isQuartoYmlPresent()).toBe(true);
});

test("rootDir triggers project render with relative projectDir", async () => {
fs.writeFileSync(
path.join(tmpDir, "_quarto.yml"),
"project:\n type: website\n",
);

let capturedCommand: string | undefined;
mockRunTerminalCommand.mockImplementation((cmd: string) => {
if (cmd === "quarto --version") {
return Promise.resolve(0);
}
capturedCommand = cmd;
return Promise.resolve(0);
});

const helper = new QuartoProjectHelper(
"index.qmd",
"index.html",
".",
tmpDir,
);
await helper.render();

// Should render the resolved absolute directory
expect(capturedCommand).toBe(`quarto render "${tmpDir}"`);
});
});

// --------------------------------------------------------------------------
// Relative vs absolute projectDir — verifies the bug where a relative
// projectDir (e.g. ".") caused isQuartoYmlPresent() to check the wrong
Expand Down
52 changes: 52 additions & 0 deletions extensions/vscode/src/utils/quartoProjectHelper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,58 @@ describe("QuartoProjectHelper", () => {
});
});

describe("rootDir resolves relative projectDir", () => {
const rootDir = path.resolve(path.sep, "workspace", "my project");

test("relative projectDir is resolved against rootDir", async () => {
mockFileExistsAt.mockResolvedValue(false);

const helper = new QuartoProjectHelper(
"index.qmd",
"index.html",
".",
rootDir,
);
expect(helper.projectDir).toBe(rootDir);
await helper.render();
expect(mockRenderCmd).toHaveBeenCalledWith(
`quarto render "${path.join(rootDir, "index.qmd")}"`,
);
});

test("nested relative projectDir is resolved against rootDir", async () => {
mockFileExistsAt.mockResolvedValue(true);

const relProjectDir = path.join("sub", "dir");
const helper = new QuartoProjectHelper(
"index.qmd",
"index.html",
relProjectDir,
rootDir,
);
const expected = path.resolve(rootDir, relProjectDir);
expect(helper.projectDir).toBe(expected);
await helper.render();
expect(mockRenderCmd).toHaveBeenCalledWith(`quarto render "${expected}"`);
});

test("absolute projectDir is unchanged when rootDir is provided", () => {
const absProjectDir = path.resolve(path.sep, "already", "absolute");
const helper = new QuartoProjectHelper(
"index.qmd",
"index.html",
absProjectDir,
rootDir,
);
expect(helper.projectDir).toBe(absProjectDir);
});

test("projectDir is unchanged when rootDir is omitted", () => {
const helper = new QuartoProjectHelper("index.qmd", "index.html", ".");
expect(helper.projectDir).toBe(".");
});
});

describe("errors", () => {
test("there is no quarto binary", async () => {
mockFileExistsAt.mockResolvedValue(false);
Expand Down
17 changes: 15 additions & 2 deletions extensions/vscode/src/utils/quartoProjectHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,23 @@ export class QuartoProjectHelper {
readonly renderedEntrypoint: string;
readonly projectDir: string;

constructor(source: string, renderedEntrypoint: string, projectDir: string) {
/**
* @param source - Source entrypoint filename (e.g. "index.qmd")
* @param renderedEntrypoint - Rendered output filename (e.g. "index.html")
* @param projectDir - Project directory (may be relative)
* @param rootDir - If provided, projectDir is resolved against this root to
* produce an absolute path. This ensures filesystem checks and quarto
* render commands work correctly regardless of the extension host's cwd.
*/
constructor(
source: string,
renderedEntrypoint: string,
projectDir: string,
rootDir?: string,
) {
this.source = source;
this.renderedEntrypoint = renderedEntrypoint;
this.projectDir = projectDir;
this.projectDir = rootDir ? path.resolve(rootDir, projectDir) : projectDir;
}

async render() {
Expand Down
1 change: 1 addition & 0 deletions extensions/vscode/src/views/homeView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,7 @@ export class HomeViewProvider implements WebviewViewProvider, Disposable {
absProjectDir,
sourceEntrypoint,
renderedEntrypoint,
this.root?.uri.fsPath,
);
}

Expand Down
2 changes: 2 additions & 0 deletions extensions/vscode/src/views/renders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const renderQuartoContent = async (
projectDir: string,
sourceEntrypoint: string,
renderedEntrypoint: string,
rootDir?: string,
) => {
try {
await window.withProgress(
Expand All @@ -26,6 +27,7 @@ export const renderQuartoContent = async (
sourceEntrypoint,
renderedEntrypoint,
projectDir,
rootDir,
);
await quartoHelper.render();
},
Expand Down
Loading