Skip to content
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
4 changes: 3 additions & 1 deletion packages/content-engine/fs/getContentDirectory.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { resolve } from "path";

export function getContentDirectory() {
return process.env.CONTENT_DIRECTORY || resolve("content");
if (process.env.CONTENT_DIRECTORY) return process.env.CONTENT_DIRECTORY;
if (process.env.TEST_MODE) return resolve("test-content");
return resolve("content");
}

export const contentDirectory = getContentDirectory();
12,952 changes: 12,952 additions & 0 deletions test-video.json

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions websites/recipe-website/editor/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ next-env.d.ts
/content
/test-content

# settings (local editor configuration)
/settings
/test-settings

# cypress
/cypress/screenshots

Expand Down
14 changes: 13 additions & 1 deletion websites/recipe-website/editor/cypress.config.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { defineConfig } from "cypress";
import { remove, copy, writeFile } from "fs-extra";
import { remove, copy, writeFile, outputJSON } from "fs-extra";
import { resolve } from "node:path";
import simpleGit from "simple-git";

async function resetData(fixture?: string) {
await remove(resolve("test-settings"));
await remove(resolve("test-content"));
if (fixture) {
await copy(
Expand Down Expand Up @@ -56,6 +57,17 @@ export default defineConfig({
},
resetData,
copyFixtures,
resolvePath(relativePath: string) {
return resolve(relativePath);
},
async writeSettings(settings: Record<string, unknown>) {
await outputJSON(
resolve("test-settings", "settings.json"),
settings,
{ spaces: 2 },
);
return null;
},
async loadGitFixture(fixture: string) {
const outputDir = resolve("test-content");
await remove(outputDir);
Expand Down
71 changes: 71 additions & 0 deletions websites/recipe-website/editor/cypress/e2e/ytdlp-import.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
describe("yt-dlp Import", function () {
beforeEach(function () {
cy.resetData("importable-uploads");
cy.visit("/new-recipe");
cy.fillSignInForm();
});

function configureMimic() {
return cy
.task<string>("resolvePath", "cypress/fixtures/yt-dlp/ytdlp-mimic.mjs")
.then((mimicPath) => {
cy.writeSettings({ ytdlpPath: mimicPath });
});
}

it("should be able to import video data with yt-dlp", function () {
configureMimic();

const url = "https://www.youtube.com/watch?v=SUCCESS_TEST";
cy.findByLabelText("Import from URL").type(url);
cy.findByRole("button", { name: "Import" }).click();

// Verify recipe form is populated with yt-dlp metadata
cy.get('[name="name"]').should("have.value", "Test Recipe Video");
cy.get('textarea[name="description"]').should(
"contain.value",
"Test Kitchen Channel",
);
cy.get('input[name="videoUrl"]').should("have.value", url);
});

it("should inform the user if yt-dlp has an error", function () {
configureMimic();

const url = "https://www.youtube.com/watch?v=ERROR_TEST";
cy.findByLabelText("Import from URL").type(url);
cy.findByRole("button", { name: "Import" }).click();

cy.contains("yt-dlp error").should("be.visible");
});

it("should inform the user if the yt-dlp binary is not present", function () {
cy.writeSettings({ ytdlpPath: "/nonexistent/path/to/yt-dlp" });

const url = "https://www.youtube.com/watch?v=SUCCESS_TEST";
cy.findByLabelText("Import from URL").type(url);
cy.findByRole("button", { name: "Import" }).click();

cy.contains("yt-dlp binary was not found").should("be.visible");
});

it("should prevent the user from submitting another request while one is currently running", function () {
configureMimic();

const url = "https://www.youtube.com/watch?v=SLOW_TEST";
cy.findByLabelText("Import from URL").type(url);
cy.findByRole("button", { name: "Import" }).click();

// Import button should be disabled while request is pending
cy.findByRole("button", { name: "Import" }).should("be.disabled");

// Wait for the slow response to complete and verify the recipe loaded
cy.get('[name="name"]', { timeout: 15000 }).should(
"have.value",
"Slow Recipe Video",
);

// Import button should be enabled again
cy.findByRole("button", { name: "Import" }).should("be.enabled");
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"url": "https://www.youtube.com/watch?v=ERROR_TEST",
"exitCode": 1,
"stderr": "ERROR: Video unavailable. This video has been removed by the uploader."
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"url": "https://www.youtube.com/watch?v=SLOW_TEST",
"delay": 3000,
"output": {
"title": "Slow Recipe Video",
"description": "A slow-loading recipe video for testing pending state.",
"thumbnail": "https://i.ytimg.com/vi/SLOW_TEST/maxresdefault.jpg",
"webpage_url": "https://www.youtube.com/watch?v=SLOW_TEST",
"duration": 600,
"channel": "Slow Kitchen Channel"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"url": "https://www.youtube.com/watch?v=SUCCESS_TEST",
"output": {
"title": "Test Recipe Video",
"description": "A test recipe video description with steps and ingredients.",
"thumbnail": "https://i.ytimg.com/vi/SUCCESS_TEST/maxresdefault.jpg",
"webpage_url": "https://www.youtube.com/watch?v=SUCCESS_TEST",
"duration": 300,
"channel": "Test Kitchen Channel"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#!/usr/bin/env node

import { readdir, readFile } from "node:fs/promises";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";

const __dirname = dirname(fileURLToPath(import.meta.url));
const responsesDir = resolve(__dirname, "responses");

const args = process.argv.slice(2);
const jIndex = args.indexOf("-J");
if (jIndex === -1 || jIndex + 1 >= args.length) {
process.stderr.write("Usage: ytdlp-mimic.mjs -J <url>\n");
process.exit(1);
}
const url = args[jIndex + 1];

const files = await readdir(responsesDir);
let matched = null;
for (const file of files) {
if (!file.endsWith(".json")) continue;
const content = JSON.parse(await readFile(resolve(responsesDir, file), "utf8"));
if (content.url === url) {
matched = content;
break;
}
}

if (!matched) {
process.stderr.write(`No fixture found for URL: ${url}\n`);
process.exit(1);
}

if (matched.delay) {
await new Promise((r) => setTimeout(r, matched.delay));
}

if (matched.exitCode && matched.exitCode !== 0) {
if (matched.stderr) {
process.stderr.write(matched.stderr);
}
process.exit(matched.exitCode);
}

if (matched.output) {
process.stdout.write(JSON.stringify(matched.output) + "\n");
}

process.exit(0);
6 changes: 6 additions & 0 deletions websites/recipe-website/editor/cypress/support/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ declare global {
signIn(user?: SignInOptions): Chainable<void>;
checkNamesInOrder(names: string[]): Chainable<void>;
copyFixtures(fixtureName: string): Chainable<void>;
writeSettings(settings: Record<string, unknown>): Chainable<void>;
}
}
}
Expand Down Expand Up @@ -102,3 +103,8 @@ Cypress.Commands.add("signIn", (options) => {
Cypress.Commands.add("copyFixtures", (fixtureName: string) => {
cy.task("copyFixtures", fixtureName);
});

Cypress.Commands.add("writeSettings", (settings: Record<string, unknown>) => {
cy.task("writeSettings", settings);
cy.request("http://localhost:3000/settings/test-invalidate-cache");
});
4 changes: 2 additions & 2 deletions websites/recipe-website/editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
"private": true,
"scripts": {
"dev": "next dev",
"dev:test": "CONTENT_DIRECTORY=test-content next dev",
"start:test": "CONTENT_DIRECTORY=test-content next start",
"dev:test": "TEST_MODE=true next dev",
"start:test": "TEST_MODE=true next start",
"build": "next build --webpack",
"start": "next start",
"lint": "next lint",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"use client";

import { TextInput } from "component-library/components/Form/inputs/Text";
import { SubmitButton } from "component-library/components/SubmitButton";
import { useActionState } from "react";
import { updateSettings } from "./actions";
import { Settings } from "@/settings";

export function SettingsForm({ settings }: { settings: Settings }) {
const [state, formAction] = useActionState(updateSettings, null);
return (
<form action={formAction}>
{state && (
<div
className={`text-sm py-1 ${state.success ? "text-green-300" : "text-red-300"}`}
>
{state.message}
</div>
)}
<TextInput
label="yt-dlp Binary Path"
name="ytdlpPath"
defaultValue={settings.ytdlpPath ?? ""}
placeholder="yt-dlp"
/>
<p className="text-xs text-gray-400 mx-2 mt-1">
Leave empty to use the default &quot;yt-dlp&quot; from PATH.
</p>
<div className="flex flex-row flex-nowrap my-1 gap-1">
<SubmitButton>Save</SubmitButton>
</div>
</form>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"use server";

import { auth } from "@/auth";
import { writeSettings } from "@/settings";

export interface SettingsActionState {
message: string;
success: boolean;
}

export async function updateSettings(
_previousState: SettingsActionState | null,
formData: FormData,
): Promise<SettingsActionState> {
const session = await auth();
if (!session?.user?.email) {
return { message: "Authentication required", success: false };
}

const ytdlpPath = formData.get("ytdlpPath");

try {
await writeSettings({
ytdlpPath:
typeof ytdlpPath === "string" && ytdlpPath ? ytdlpPath : undefined,
});
return { message: "Settings saved.", success: true };
} catch {
return { message: "Failed to save settings.", success: false };
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { rebuildRecipeIndex } from "recipe-editor/controller/actions";
import { rebuildFeaturedRecipeIndex } from "recipe-editor/controller/actions/featuredRecipes";
import { auth, signIn } from "@/auth";
import { readSettings } from "@/settings";
import { SubmitButton } from "component-library/components/SubmitButton";
import {
PageMain,
PageSection,
PageHeading,
} from "recipe-website-common/components/PageLayout";
import { SettingsForm } from "./SettingsForm";

export default async function SettingsPage() {
const user = await auth();
Expand All @@ -15,8 +17,15 @@ export default async function SettingsPage() {
redirectTo: `/settings`,
});
}
const settings = await readSettings();
return (
<PageMain>
<PageSection maxWidth="xl" grow>
<PageHeading>Tools</PageHeading>
<div className="my-4 mx-2">
<SettingsForm settings={settings} />
</div>
</PageSection>
<PageSection maxWidth="xl" grow>
<PageHeading>Database</PageHeading>
<div className="my-4 mx-2">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { revalidatePath } from "next/cache";

export async function GET() {
// Only allow in test environment
if (process.env.CONTENT_DIRECTORY !== "test-content") {
if (!process.env.TEST_MODE) {
return Response.json({ error: "Not available" }, { status: 404 });
}

Expand Down
Loading