Repository: gradio-app/gradio

Issue:
Fix `test/test_gradio_component_cli.py:141 ` that breaks only on release PR
### Describe the bug

On every release PR, there's a backend test that fails because the JS packages have not been published yet. 

E.g. https://github.com/gradio-app/gradio/actions/runs/7093296419/job/19306412188?pr=6575

<img width="1020" alt="image" src="https://github.com/gradio-app/gradio/assets/1778297/2d70e035-cf86-4726-b45d-134bbad1cb02">

It'd be great if we can fix this so that we can rely on this test when releasing

### Have you searched existing issues?  🔎

- [X] I have searched and found no existing issues

### Reproduction

https://github.com/gradio-app/gradio/actions/runs/7093296419/job/19306412188?pr=6575

### Screenshot

_No response_

### Logs

_No response_

### System Info

```shell
gradio==4.7.1
```


### Severity

I can work around it


Discussion:

Resolving patch:
diff --git a/gradio/cli/commands/components/_create_utils.py b/gradio/cli/commands/components/_create_utils.py
--- a/gradio/cli/commands/components/_create_utils.py
+++ b/gradio/cli/commands/components/_create_utils.py
@@ -13,8 +13,8 @@
 
 
 def _in_test_dir():
-    """Check if the current working directory ends with gradio/js/gradio-preview/test."""
-    return Path.cwd().parts[-4:] == ("gradio", "js", "gradio-preview", "test")
+    """Check if the current working directory ends with gradio/js/preview/test."""
+    return Path.cwd().parts[-4:] == ("gradio", "js", "preview", "test")
 
 
 default_demo_code = """
diff --git a/gradio/cli/commands/components/build.py b/gradio/cli/commands/components/build.py
--- a/gradio/cli/commands/components/build.py
+++ b/gradio/cli/commands/components/build.py
@@ -1,6 +1,5 @@
 import shutil
 import subprocess
-import sys
 from pathlib import Path
 
 import semantic_version
@@ -83,7 +82,7 @@ def _build(
             else:
                 live.update(":white_check_mark: Build succeeded!")
 
-        cmds = [sys.executable, "-m", "build", str(name)]
+        cmds = [shutil.which("python"), "-m", "build", str(name)]
         live.update(f":construction_worker: Building... [grey37]({' '.join(cmds)})[/]")
         pipe = subprocess.run(cmds, capture_output=True, text=True)
         if pipe.returncode != 0:
diff --git a/gradio/cli/commands/components/publish.py b/gradio/cli/commands/components/publish.py
--- a/gradio/cli/commands/components/publish.py
+++ b/gradio/cli/commands/components/publish.py
@@ -1,11 +1,9 @@
-from __future__ import annotations
-
 import random
 import re
 import shutil
 import tempfile
 from pathlib import Path
-from typing import Optional
+from typing import List, Optional
 
 import semantic_version
 from huggingface_hub import HfApi
@@ -65,13 +63,13 @@ def _ignore(s, names):
     return ignored
 
 
-def _get_version_from_file(dist_file: Path) -> str | None:
+def _get_version_from_file(dist_file: Path) -> Optional[str]:
     match = re.search(r"-(\d+\.\d+\.\d+[a-zA-Z]*\d*)-", dist_file.name)
     if match:
         return match.group(1)
 
 
-def _get_max_version(distribution_files: list[Path]) -> str | None:
+def _get_max_version(distribution_files: List[Path]) -> Optional[str]:
     versions = []
     for p in distribution_files:
         version = _get_version_from_file(p)


Test patch:
diff --git a/js/app/test/audio_component_events.spec.ts b/js/app/test/audio_component_events.spec.ts
--- a/js/app/test/audio_component_events.spec.ts
+++ b/js/app/test/audio_component_events.spec.ts
@@ -7,10 +7,7 @@ test("Audio click-to-upload uploads audio successfuly. File downloading works an
 		.getByRole("button", { name: "Drop Audio Here - or - Click to Upload" })
 		.click();
 	const uploader = await page.locator("input[type=file]");
-	await Promise.all([
-		uploader.setInputFiles(["../../test/test_files/audio_sample.wav"]),
-		page.waitForResponse("**/upload?*")
-	]);
+	await uploader.setInputFiles(["../../test/test_files/audio_sample.wav"]);
 
 	await expect(page.getByLabel("# Input Change Events")).toHaveValue("1");
 	await expect(page.getByLabel("# Input Upload Events")).toHaveValue("1");
@@ -21,10 +18,7 @@ test("Audio click-to-upload uploads audio successfuly. File downloading works an
 		.getByRole("button", { name: "Drop Audio Here - or - Click to Upload" })
 		.click();
 
-	await Promise.all([
-		uploader.setInputFiles(["../../test/test_files/audio_sample.wav"]),
-		page.waitForResponse("**/upload?*")
-	]);
+	await uploader.setInputFiles(["../../test/test_files/audio_sample.wav"]);
 
 	await expect(page.getByLabel("# Input Change Events")).toHaveValue("3");
 	await expect(page.getByLabel("# Input Upload Events")).toHaveValue("2");
@@ -38,16 +32,13 @@ test("Audio click-to-upload uploads audio successfuly. File downloading works an
 test("Audio drag-and-drop uploads a file to the server correctly.", async ({
 	page
 }) => {
-	await Promise.all([
-		drag_and_drop_file(
-			page,
-			"input[type=file]",
-			"../../test/test_files/audio_sample.wav",
-			"audio_sample.wav",
-			"audio/wav"
-		),
-		page.waitForResponse("**/upload?*")
-	]);
+	await drag_and_drop_file(
+		page,
+		"input[type=file]",
+		"../../test/test_files/audio_sample.wav",
+		"audio_sample.wav",
+		"audio/wav"
+	);
 	await expect(page.getByLabel("# Input Change Events")).toHaveValue("1");
 	await expect(page.getByLabel("# Input Upload Events")).toHaveValue("1");
 });
@@ -67,30 +58,24 @@ test("Audio drag-and-drop displays a warning when the file is of the wrong mime
 
 test("Play, Pause, and stop events work correctly.", async ({ page }) => {
 	const uploader = await page.locator("input[type=file]");
-	await Promise.all([
-		uploader.setInputFiles(["../../demo/audio_debugger/cantina.wav"]),
-		page.waitForResponse("**/upload?*")
-	]);
+	await uploader.setInputFiles(["../../demo/audio_debugger/cantina.wav"]);
+	const event_triggered = async (label: string) => {
+		const value = await page.getByLabel(label).inputValue();
+		expect(Number(value)).toBeGreaterThan(0);
+	};
 
 	await page
 		.getByTestId("waveform-Output Audio")
 		.getByLabel("Play", { exact: true })
 		.click();
+	await expect(async () => event_triggered("# Output Play Events")).toPass();
+
 	await page.getByTestId("waveform-Output Audio").getByLabel("Pause").click();
+	await expect(async () => event_triggered("# Output Pause Events")).toPass();
+
 	await page
 		.getByTestId("waveform-Output Audio")
 		.getByLabel("Play", { exact: true })
 		.click();
-
-	const event_triggered = async (label: string) => {
-		const value = await page.getByLabel(label).inputValue();
-		expect(Number(value)).toBeGreaterThan(0);
-	};
-
-	// toPass will retry the function until it passes or times out
-	// need this because the stop event is only triggered when the video is done playing
-	// hard to time otherwise
-	await expect(async () => event_triggered("# Output Play Events")).toPass();
-	await expect(async () => event_triggered("# Output Pause Events")).toPass();
 	await expect(async () => event_triggered("# Output Stop Events")).toPass();
 });
diff --git a/js/app/test/chatinterface_streaming_echo.spec.ts b/js/app/test/chatinterface_streaming_echo.spec.ts
--- a/js/app/test/chatinterface_streaming_echo.spec.ts
+++ b/js/app/test/chatinterface_streaming_echo.spec.ts
@@ -9,51 +9,30 @@ test("chatinterface works with streaming functions and all buttons behave as exp
 	const clear_button = await page.getByRole("button", { name: "🗑️ Clear" });
 	const textbox = await page.getByPlaceholder("Type a message...");
 
-	let iterations: Promise<any>[] = [];
-	page.on("websocket", (ws) => {
-		iterations.push(
-			ws.waitForEvent("framereceived", {
-				predicate: (event) => {
-					return (
-						JSON.parse(event.payload as string).msg === "process_completed"
-					);
-				}
-			})
-		);
-	});
-
 	await textbox.fill("hello");
 	await submit_button.click();
-	await iterations[0];
 	await expect(textbox).toHaveValue("");
-	await expect.poll(async () => page.locator(".chatbot p").count()).toBe(1);
 	const bot_message_0 = await page.locator(".bot.message").nth(0);
 	await expect(bot_message_0).toContainText("You typed: hello");
 
 	await textbox.fill("hi");
 	await submit_button.click();
-	await iterations[1];
 	await expect(textbox).toHaveValue("");
-	await expect.poll(async () => page.locator(".message.bot").count()).toBe(2);
 	const bot_message_1 = await page.locator(".bot").nth(1);
 	await expect(bot_message_1).toContainText("You typed: hi");
 
 	await retry_button.click();
-	await iterations[2];
 	await expect(textbox).toHaveValue("");
-	await expect(bot_message_1).toContainText("You typed: hi");
+	await expect(page.locator(".bot").nth(1)).toContainText("You typed: hi");
 
 	await undo_button.click();
-	await iterations[3];
 	await expect.poll(async () => page.locator(".message.bot").count()).toBe(1);
 	await expect(textbox).toHaveValue("hi");
 
 	await textbox.fill("salaam");
 	await submit_button.click();
-	await iterations[4];
 	await expect(textbox).toHaveValue("");
-	await expect.poll(async () => page.locator(".bot.message").count()).toBe(2);
-	await expect(bot_message_1).toContainText("You typed: salaam");
+	await expect(page.locator(".bot").nth(1)).toContainText("You typed: salaam");
 
 	await clear_button.click();
 	await expect.poll(async () => page.locator(".bot.message").count()).toBe(0);
diff --git a/js/app/test/image_component_events.spec.ts b/js/app/test/image_component_events.spec.ts
--- a/js/app/test/image_component_events.spec.ts
+++ b/js/app/test/image_component_events.spec.ts
@@ -6,37 +6,34 @@ test("Image click-to-upload uploads image successfuly. Clear button dispatches e
 }) => {
 	await page.getByRole("button", { name: "Drop Image Here" }).click();
 	const uploader = await page.locator("input[type=file]");
-	await Promise.all([
-		uploader.setInputFiles(["./test/files/cheetah1.jpg"]),
-		page.waitForResponse("**/upload?*?*")
-	]);
+	const change_counter = await page.getByLabel("# Change Events", {
+		exact: true
+	});
+	const clear_counter = await page.getByLabel("# Clear Events");
+	const upload_counter = await page.getByLabel("# Upload Events");
+	const change_output_counter = await page.getByLabel("# Change Events Output");
 
-	await expect(page.getByLabel("# Change Events").first()).toHaveValue("1");
-	await expect(await page.getByLabel("# Upload Events")).toHaveValue("1");
-	await expect(await page.getByLabel("# Change Events Output")).toHaveValue(
-		"1"
-	);
+	await uploader.setInputFiles("./test/files/cheetah1.jpg");
+
+	await expect(change_counter).toHaveValue("1");
+	await expect(upload_counter).toHaveValue("1");
+	await expect(change_output_counter).toHaveValue("1");
 
 	const downloadPromise = page.waitForEvent("download");
 	await page.getByLabel("Download").click();
 	const download = await downloadPromise;
-	// Automatically convert to png in the backend since PIL is very picky
+	// PIL converts from .jpg to .jpeg
 	await expect(download.suggestedFilename()).toBe("cheetah1.jpeg");
 
 	await page.getByLabel("Remove Image").click();
-	await expect(page.getByLabel("# Clear Events")).toHaveValue("1");
-	await expect(page.getByLabel("# Change Events").first()).toHaveValue("2");
+	await expect(clear_counter).toHaveValue("1");
+	await expect(change_counter).toHaveValue("2");
+	await expect(upload_counter).toHaveValue("1");
 
-	await Promise.all([
-		uploader.setInputFiles(["./test/files/gradio-logo.svg"]),
-		page.waitForResponse("**/upload?*?*")
-	]);
-
-	await expect(page.getByLabel("# Change Events").first()).toHaveValue("3");
-	await expect(await page.getByLabel("# Upload Events")).toHaveValue("2");
-	await expect(await page.getByLabel("# Change Events Output")).toHaveValue(
-		"2"
-	);
+	await uploader.setInputFiles("./test/files/gradio-logo.svg");
+	await expect(change_counter).toHaveValue("3");
+	await expect(upload_counter).toHaveValue("2");
+	await expect(change_output_counter).toHaveValue("2");
 
 	const SVGdownloadPromise = page.waitForEvent("download");
 	await page.getByLabel("Download").click();
@@ -45,16 +42,13 @@ test("Image click-to-upload uploads image successfuly. Clear button dispatches e
 });
 
 test("Image drag-to-upload uploads image successfuly.", async ({ page }) => {
-	await Promise.all([
-		drag_and_drop_file(
-			page,
-			"input[type=file]",
-			"./test/files/cheetah1.jpg",
-			"cheetag1.jpg",
-			"image/*"
-		),
-		page.waitForResponse("**/upload?*")
-	]);
+	await drag_and_drop_file(
+		page,
+		"input[type=file]",
+		"./test/files/cheetah1.jpg",
+		"cheetag1.jpg",
+		"image/*"
+	);
 	await expect(page.getByLabel("# Change Events").first()).toHaveValue("1");
 	await expect(page.getByLabel("# Upload Events")).toHaveValue("1");
 });
@@ -71,9 +65,7 @@ test("Image copy from clipboard dispatches upload event.", async ({ page }) => {
 		navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]);
 	});
 
-	await page.pause();
 	await page.getByLabel("clipboard-image-toolbar-btn").click();
-	await page.pause();
 	await expect(page.getByLabel("# Change Events").first()).toHaveValue("1");
 	await expect(page.getByLabel("# Upload Events")).toHaveValue("1");
 });
diff --git a/js/app/test/model3d_component_events.spec.ts b/js/app/test/model3d_component_events.spec.ts
--- a/js/app/test/model3d_component_events.spec.ts
+++ b/js/app/test/model3d_component_events.spec.ts
@@ -7,18 +7,19 @@ test("Model3D click-to-upload uploads file successfuly. Upload and clear events
 		.getByRole("button", { name: "Drop File Here - or - Click to Upload" })
 		.click();
 	const uploader = await page.locator("input[type=file]");
-	await Promise.all([
-		uploader.setInputFiles(["./test/files/face.obj"]),
-		page.waitForResponse("**/upload?*?*")
-	]);
+	const change_counter = await page.getByLabel("# Change Events");
+	const upload_counter = await page.getByLabel("# Upload Events");
+	const clear_counter = await page.getByLabel("# Clear Events");
 
-	await expect(page.getByLabel("# Change Events")).toHaveValue("1");
-	await expect(page.getByLabel("# Upload Events")).toHaveValue("1");
+	await uploader.setInputFiles("./test/files/face.obj");
+
+	await expect(change_counter).toHaveValue("1");
+	await expect(upload_counter).toHaveValue("1");
 
 	await page.getByLabel("Clear").nth(0).click();
-	await expect(page.getByLabel("# Change Events")).toHaveValue("2");
-	await expect(page.getByLabel("# Clear Events")).toHaveValue("1");
-	await expect(page.getByLabel("Clear Value")).toHaveValue("");
+	await expect(change_counter).toHaveValue("2");
+	await expect(clear_counter).toHaveValue("1");
+	await expect(await page.getByLabel("Clear Value")).toHaveValue("");
 
 	const downloadPromise = page.waitForEvent("download");
 	await page.getByLabel("Download").click();
@@ -35,7 +36,6 @@ test("Model3D drag-and-drop uploads a file to the server correctly.", async ({
 		"./test/files/face.obj",
 		"face.obj"
 	);
-	await page.waitForResponse("**/upload?*");
-	await expect(page.getByLabel("# Change Events")).toHaveValue("1");
-	await expect(page.getByLabel("# Upload Events")).toHaveValue("1");
+	await expect(await page.getByLabel("# Change Events")).toHaveValue("1");
+	await expect(await page.getByLabel("# Upload Events")).toHaveValue("1");
 });
diff --git a/js/app/test/upload_button_component_events.spec.ts b/js/app/test/upload_button_component_events.spec.ts
--- a/js/app/test/upload_button_component_events.spec.ts
+++ b/js/app/test/upload_button_component_events.spec.ts
@@ -5,10 +5,7 @@ test("UploadButton properly dispatches load event and click event for the single
 }) => {
 	await page.getByRole("button", { name: "Upload Single File" }).click();
 	const uploader = await page.getByTestId("Upload Single File-upload-button");
-	await Promise.all([
-		uploader.setInputFiles(["./test/files/face.obj"]),
-		page.waitForResponse("**/upload?*?*")
-	]);
+	await uploader.setInputFiles(["./test/files/cheetah1.jpg"]);
 
 	await expect(page.getByLabel("# Load Upload Single File")).toHaveValue("1");
 	await expect(
@@ -18,7 +15,7 @@ test("UploadButton properly dispatches load event and click event for the single
 	const downloadPromise = page.waitForEvent("download");
 	await page.getByRole("link").nth(0).click();
 	const download = await downloadPromise;
-	await expect(download.suggestedFilename()).toBe("face.obj");
+	await expect(download.suggestedFilename()).toBe("cheetah1.jpg");
 });
 
 test("UploadButton properly dispatches load event and click event for the multiple file case.", async ({
@@ -28,12 +25,9 @@ test("UploadButton properly dispatches load event and click event for the multip
 	const uploader = await page.getByTestId(
 		"Upload Multiple Files-upload-button"
 	);
-	await Promise.all([
-		uploader.setInputFiles([
-			"./test/files/face.obj",
-			"./test/files/cheetah1.jpg"
-		]),
-		page.waitForResponse("**/upload?*?*")
+	await uploader.setInputFiles([
+		"./test/files/face.obj",
+		"./test/files/cheetah1.jpg"
 	]);
 
 	await expect(page.getByLabel("# Load Upload Multiple Files")).toHaveValue(
diff --git a/js/app/test/video_component_events.spec.ts b/js/app/test/video_component_events.spec.ts
--- a/js/app/test/video_component_events.spec.ts
+++ b/js/app/test/video_component_events.spec.ts
@@ -7,44 +7,44 @@ test("Video click-to-upload uploads video successfuly. Clear, play, and pause bu
 		.getByRole("button", { name: "Drop Video Here - or - Click to Upload" })
 		.click();
 	const uploader = await page.locator("input[type=file]");
-	await Promise.all([
-		uploader.setInputFiles(["./test/files/file_test.ogg"]),
-		page.waitForResponse("**/upload?*?*")
-	]);
+	await uploader.setInputFiles(["./test/files/file_test.ogg"]);
 
 	await expect(page.getByLabel("# Change Events")).toHaveValue("1");
 	await expect(page.getByLabel("# Upload Events")).toHaveValue("1");
 
-	await page.getByLabel("play-pause-replay-button").nth(0).click();
-	await page.getByLabel("play-pause-replay-button").nth(0).click();
-	await expect(page.getByLabel("# Play Events")).toHaveValue("1");
-	await expect(page.getByLabel("# Pause Events")).toHaveValue("1");
-
 	await page.getByLabel("Clear").click();
 	await expect(page.getByLabel("# Change Events")).toHaveValue("2");
 	await page
 		.getByRole("button", { name: "Drop Video Here - or - Click to Upload" })
 		.click();
 
-	await Promise.all([
-		uploader.setInputFiles(["./test/files/file_test.ogg"]),
-		page.waitForResponse("**/upload?*")
-	]);
+	await uploader.setInputFiles(["./test/files/file_test.ogg"]);
 
 	await expect(page.getByLabel("# Change Events")).toHaveValue("3");
 	await expect(page.getByLabel("# Upload Events")).toHaveValue("2");
 
-	await page.getByLabel("play-pause-replay-button").first().click();
-	await page.getByLabel("play-pause-replay-button").first().click();
-	await expect(page.getByLabel("# Play Events")).toHaveValue("2");
-	await expect(page.getByLabel("# Pause Events")).toHaveValue("2");
-
 	const downloadPromise = page.waitForEvent("download");
 	await page.getByLabel("Download").click();
 	const download = await downloadPromise;
 	await expect(download.suggestedFilename()).toBe("file_test.ogg");
 });
 
+test("Video play, pause events work correctly.", async ({ page }) => {
+	await page
+		.getByRole("button", { name: "Drop Video Here - or - Click to Upload" })
+		.click();
+	const uploader = await page.locator("input[type=file]");
+	await uploader.setInputFiles(["./test/files/file_test.ogg"]);
+
+	// Wait change event to trigger
+	await expect(page.getByLabel("# Change Events")).toHaveValue("1");
+
+	await page.getByLabel("play-pause-replay-button").first().click();
+	await expect(page.getByLabel("# Play Events")).toHaveValue("1");
+	await page.getByLabel("play-pause-replay-button").first().click();
+	await expect(page.getByLabel("# Pause Events")).toHaveValue("1");
+});
+
 test("Video drag-and-drop uploads a file to the server correctly.", async ({
 	page
 }) => {
@@ -55,7 +55,6 @@ test("Video drag-and-drop uploads a file to the server correctly.", async ({
 		"file_test.ogg",
 		"video/*"
 	);
-	await page.waitForResponse("**/upload?*");
 	await expect(page.getByLabel("# Change Events")).toHaveValue("1");
 	await expect(page.getByLabel("# Upload Events")).toHaveValue("1");
 });
diff --git a/test/requirements.txt b/test/requirements.txt
--- a/test/requirements.txt
+++ b/test/requirements.txt
@@ -141,6 +141,7 @@ pytest==7.1.2
     #   -r requirements.in
     #   pytest-asyncio
     #   pytest-cov
+pytest-virtualenv==1.7.0
 pytest-asyncio==0.19.0
     # via -r requirements.in
 pytest-cov==3.0.0
diff --git a/test/test_gradio_component_cli.py b/test/test_gradio_component_cli.py
--- a/test/test_gradio_component_cli.py
+++ b/test/test_gradio_component_cli.py
@@ -1,12 +1,11 @@
+import shutil
 import textwrap
 from pathlib import Path
 
 import pytest
 
 from gradio.cli.commands.components._create_utils import OVERRIDES
-from gradio.cli.commands.components.build import _build
 from gradio.cli.commands.components.create import _create
-from gradio.cli.commands.components.install_component import _install
 from gradio.cli.commands.components.publish import _get_version_from_file
 from gradio.cli.commands.components.show import _show
 
@@ -107,38 +106,44 @@ def test_show(capsys):
 
 
 @pytest.mark.xfail
-@pytest.mark.parametrize("template", ["Audio", "Video", "Image", "Textbox"])
-def test_build(template, tmp_path):
-    _create(
-        "TestTextbox",
-        template=template,
-        directory=tmp_path,
-        overwrite=True,
-        install=True,
-        configure_metadata=False,
-    )
-    _build(tmp_path, build_frontend=True)
-    template_dir: Path = (
-        tmp_path.resolve() / "backend" / "gradio_testtextbox" / "templates"
-    )
-    assert template_dir.exists() and template_dir.is_dir()
-    assert list(template_dir.glob("**/index.js"))
-    assert (tmp_path / "dist").exists() and list((tmp_path / "dist").glob("*.whl"))
-
-
-def test_install(tmp_path):
-    _create(
-        "TestTextbox",
-        template="Textbox",
-        directory=tmp_path,
-        overwrite=True,
-        install=False,
-        configure_metadata=False,
-    )
-
-    assert not (tmp_path / "frontend" / "node_modules").exists()
-    _install(tmp_path)
-    assert (tmp_path / "frontend" / "node_modules").exists()
+@pytest.mark.parametrize("template", ["Image"])
+def test_build(template, virtualenv):
+    # Copy pnpm-lock.yaml to not cause unintended changes tracked by git
+    pnpm_lock = Path(__file__).parent / ".." / "pnpm-lock.yaml"
+    pnpm_copy = Path(__file__).parent / ".." / "pnpm-lock-copy.yaml"
+    shutil.copy(str(pnpm_lock), str(pnpm_copy))
+
+    # Using the js/preview/test directory will use the workspace code
+    dir_ = (
+        Path(__file__).parent / ".." / "js" / "preview" / "test" / "testtextbox"
+    ).resolve()
+    shutil.rmtree(str(dir_), ignore_errors=True)
+
+    try:
+        # Local installs of gradio and gradio-client
+        gradio_dir = Path(__file__).parent / ".."
+        client = Path(__file__).parent / ".." / "client" / "python"
+        virtualenv.run("pip install build")
+        virtualenv.run(f"pip install -e {str(gradio_dir)}")
+        virtualenv.run(f"pip install -e {str(client)}")
+
+        virtualenv.run(
+            f"{shutil.which('gradio')} cc create TestTextbox --template {template} --no-configure-metadata --directory {str(dir_)}",
+        )
+        assert (dir_ / "frontend" / "node_modules").exists()
+
+        # need to reinstall local client because installing the custom component
+        # will pull latest stable version from pypi
+        virtualenv.run(f"pip install -e {str(client)}")
+        virtualenv.run(f"{shutil.which('gradio')} cc build {str(dir_)}")
+
+        template_dir: Path = dir_ / "backend" / "gradio_testtextbox" / "templates"
+        assert template_dir.exists() and template_dir.is_dir()
+        assert list(template_dir.glob("**/index.js"))
+        assert (dir_ / "dist").exists() and list((dir_ / "dist").glob("*.whl"))
+    finally:
+        shutil.move(str(pnpm_copy), str(pnpm_lock))
+        shutil.rmtree(str(dir_), ignore_errors=True)
 
 
 def test_fallback_template_app(tmp_path):