Compare commits
11 Commits
abhi/marke
...
gitbook
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fdb7ff8111 | ||
|
|
0e42efb7d5 | ||
|
|
f2d82d8802 | ||
|
|
f9f984a8f4 | ||
|
|
446c71fec8 | ||
|
|
ec4c2caa14 | ||
|
|
516e8b4b25 | ||
|
|
e7e118b5a8 | ||
|
|
92a7a7e6d6 | ||
|
|
e16995347f | ||
|
|
234d3acb4c |
@@ -1,12 +1,37 @@
|
||||
-- CreateExtension
|
||||
-- Supabase: pgvector must be enabled via Dashboard → Database → Extensions first
|
||||
-- Creates extension in current schema (determined by search_path from DATABASE_URL ?schema= param)
|
||||
-- Ensures vector extension is in the current schema (from DATABASE_URL ?schema= param)
|
||||
-- If it exists in a different schema (e.g., public), we drop and recreate it in the current schema
|
||||
-- This ensures vector type is in the same schema as tables, making ::vector work without explicit qualification
|
||||
DO $$
|
||||
DECLARE
|
||||
current_schema_name text;
|
||||
vector_schema text;
|
||||
BEGIN
|
||||
CREATE EXTENSION IF NOT EXISTS "vector";
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
RAISE NOTICE 'vector extension not available or already exists, skipping';
|
||||
-- Get the current schema from search_path
|
||||
SELECT current_schema() INTO current_schema_name;
|
||||
|
||||
-- Check if vector extension exists and which schema it's in
|
||||
SELECT n.nspname INTO vector_schema
|
||||
FROM pg_extension e
|
||||
JOIN pg_namespace n ON e.extnamespace = n.oid
|
||||
WHERE e.extname = 'vector';
|
||||
|
||||
-- Handle removal if in wrong schema
|
||||
IF vector_schema IS NOT NULL AND vector_schema != current_schema_name THEN
|
||||
BEGIN
|
||||
-- Vector exists in a different schema, drop it first
|
||||
RAISE WARNING 'pgvector found in schema "%" but need it in "%". Dropping and reinstalling...',
|
||||
vector_schema, current_schema_name;
|
||||
EXECUTE 'DROP EXTENSION IF EXISTS vector CASCADE';
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
RAISE EXCEPTION 'Failed to drop pgvector from schema "%": %. You may need to drop it manually.',
|
||||
vector_schema, SQLERRM;
|
||||
END;
|
||||
END IF;
|
||||
|
||||
-- Create extension in current schema (let it fail naturally if not available)
|
||||
EXECUTE format('CREATE EXTENSION IF NOT EXISTS vector SCHEMA %I', current_schema_name);
|
||||
END $$;
|
||||
|
||||
-- CreateEnum
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
-- Acknowledge Supabase-managed extensions to prevent drift warnings
|
||||
-- These extensions are pre-installed by Supabase in specific schemas
|
||||
-- This migration ensures they exist where available (Supabase) or skips gracefully (CI)
|
||||
|
||||
-- Create schemas (safe in both CI and Supabase)
|
||||
CREATE SCHEMA IF NOT EXISTS "extensions";
|
||||
|
||||
-- Extensions that exist in both CI and Supabase
|
||||
DO $$
|
||||
BEGIN
|
||||
CREATE EXTENSION IF NOT EXISTS "pgcrypto" WITH SCHEMA "extensions";
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
RAISE NOTICE 'pgcrypto extension not available, skipping';
|
||||
END $$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA "extensions";
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
RAISE NOTICE 'uuid-ossp extension not available, skipping';
|
||||
END $$;
|
||||
|
||||
-- Supabase-specific extensions (skip gracefully in CI)
|
||||
DO $$
|
||||
BEGIN
|
||||
CREATE EXTENSION IF NOT EXISTS "pg_stat_statements" WITH SCHEMA "extensions";
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
RAISE NOTICE 'pg_stat_statements extension not available, skipping';
|
||||
END $$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
CREATE EXTENSION IF NOT EXISTS "pg_net" WITH SCHEMA "extensions";
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
RAISE NOTICE 'pg_net extension not available, skipping';
|
||||
END $$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
CREATE EXTENSION IF NOT EXISTS "pgjwt" WITH SCHEMA "extensions";
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
RAISE NOTICE 'pgjwt extension not available, skipping';
|
||||
END $$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
CREATE SCHEMA IF NOT EXISTS "graphql";
|
||||
CREATE EXTENSION IF NOT EXISTS "pg_graphql" WITH SCHEMA "graphql";
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
RAISE NOTICE 'pg_graphql extension not available, skipping';
|
||||
END $$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
CREATE SCHEMA IF NOT EXISTS "pgsodium";
|
||||
CREATE EXTENSION IF NOT EXISTS "pgsodium" WITH SCHEMA "pgsodium";
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
RAISE NOTICE 'pgsodium extension not available, skipping';
|
||||
END $$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
CREATE SCHEMA IF NOT EXISTS "vault";
|
||||
CREATE EXTENSION IF NOT EXISTS "supabase_vault" WITH SCHEMA "vault";
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
RAISE NOTICE 'supabase_vault extension not available, skipping';
|
||||
END $$;
|
||||
|
||||
|
||||
-- Return to platform
|
||||
CREATE SCHEMA IF NOT EXISTS "platform";
|
||||
@@ -34,7 +34,10 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
# Default output directory relative to repo root
|
||||
DEFAULT_OUTPUT_DIR = (
|
||||
Path(__file__).parent.parent.parent.parent / "docs" / "integrations"
|
||||
Path(__file__).parent.parent.parent.parent
|
||||
/ "docs"
|
||||
/ "integrations"
|
||||
/ "block-integrations"
|
||||
)
|
||||
|
||||
|
||||
@@ -421,6 +424,14 @@ def generate_block_markdown(
|
||||
lines.append("<!-- END MANUAL -->")
|
||||
lines.append("")
|
||||
|
||||
# Optional per-block extras (only include if has content)
|
||||
extras = manual_content.get("extras", "")
|
||||
if extras:
|
||||
lines.append("<!-- MANUAL: extras -->")
|
||||
lines.append(extras)
|
||||
lines.append("<!-- END MANUAL -->")
|
||||
lines.append("")
|
||||
|
||||
lines.append("---")
|
||||
lines.append("")
|
||||
|
||||
@@ -456,25 +467,52 @@ def get_block_file_mapping(blocks: list[BlockDoc]) -> dict[str, list[BlockDoc]]:
|
||||
return dict(file_mapping)
|
||||
|
||||
|
||||
def generate_overview_table(blocks: list[BlockDoc]) -> str:
|
||||
"""Generate the overview table markdown (blocks.md)."""
|
||||
def generate_overview_table(blocks: list[BlockDoc], block_dir_prefix: str = "") -> str:
|
||||
"""Generate the overview table markdown (blocks.md).
|
||||
|
||||
Args:
|
||||
blocks: List of block documentation objects
|
||||
block_dir_prefix: Prefix for block file links (e.g., "block-integrations/")
|
||||
"""
|
||||
lines = []
|
||||
|
||||
# GitBook YAML frontmatter
|
||||
lines.append("---")
|
||||
lines.append("layout:")
|
||||
lines.append(" width: default")
|
||||
lines.append(" title:")
|
||||
lines.append(" visible: true")
|
||||
lines.append(" description:")
|
||||
lines.append(" visible: true")
|
||||
lines.append(" tableOfContents:")
|
||||
lines.append(" visible: false")
|
||||
lines.append(" outline:")
|
||||
lines.append(" visible: true")
|
||||
lines.append(" pagination:")
|
||||
lines.append(" visible: true")
|
||||
lines.append(" metadata:")
|
||||
lines.append(" visible: true")
|
||||
lines.append("---")
|
||||
lines.append("")
|
||||
|
||||
lines.append("# AutoGPT Blocks Overview")
|
||||
lines.append("")
|
||||
lines.append(
|
||||
'AutoGPT uses a modular approach with various "blocks" to handle different tasks. These blocks are the building blocks of AutoGPT workflows, allowing users to create complex automations by combining simple, specialized components.'
|
||||
)
|
||||
lines.append("")
|
||||
lines.append('!!! info "Creating Your Own Blocks"')
|
||||
lines.append(" Want to create your own custom blocks? Check out our guides:")
|
||||
lines.append(" ")
|
||||
lines.append('{% hint style="info" %}')
|
||||
lines.append("**Creating Your Own Blocks**")
|
||||
lines.append("")
|
||||
lines.append("Want to create your own custom blocks? Check out our guides:")
|
||||
lines.append("")
|
||||
lines.append(
|
||||
" - [Build your own Blocks](https://docs.agpt.co/platform/new_blocks/) - Step-by-step tutorial with examples"
|
||||
"* [Build your own Blocks](https://docs.agpt.co/platform/new_blocks/) - Step-by-step tutorial with examples"
|
||||
)
|
||||
lines.append(
|
||||
" - [Block SDK Guide](https://docs.agpt.co/platform/block-sdk-guide/) - Advanced SDK patterns with OAuth, webhooks, and provider configuration"
|
||||
"* [Block SDK Guide](https://docs.agpt.co/platform/block-sdk-guide/) - Advanced SDK patterns with OAuth, webhooks, and provider configuration"
|
||||
)
|
||||
lines.append("{% endhint %}")
|
||||
lines.append("")
|
||||
lines.append(
|
||||
"Below is a comprehensive list of all available blocks, categorized by their primary function. Click on any block name to view its detailed documentation."
|
||||
@@ -537,7 +575,8 @@ def generate_overview_table(blocks: list[BlockDoc]) -> str:
|
||||
else "No description"
|
||||
)
|
||||
short_desc = short_desc.replace("\n", " ").replace("|", "\\|")
|
||||
lines.append(f"| [{block.name}]({file_path}#{anchor}) | {short_desc} |")
|
||||
link_path = f"{block_dir_prefix}{file_path}"
|
||||
lines.append(f"| [{block.name}]({link_path}#{anchor}) | {short_desc} |")
|
||||
lines.append("")
|
||||
continue
|
||||
|
||||
@@ -563,13 +602,55 @@ def generate_overview_table(blocks: list[BlockDoc]) -> str:
|
||||
)
|
||||
short_desc = short_desc.replace("\n", " ").replace("|", "\\|")
|
||||
|
||||
lines.append(f"| [{block.name}]({file_path}#{anchor}) | {short_desc} |")
|
||||
link_path = f"{block_dir_prefix}{file_path}"
|
||||
lines.append(f"| [{block.name}]({link_path}#{anchor}) | {short_desc} |")
|
||||
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def generate_summary_md(
|
||||
blocks: list[BlockDoc], root_dir: Path, block_dir_prefix: str = ""
|
||||
) -> str:
|
||||
"""Generate SUMMARY.md for GitBook navigation.
|
||||
|
||||
Args:
|
||||
blocks: List of block documentation objects
|
||||
root_dir: The root docs directory (e.g., docs/integrations/)
|
||||
block_dir_prefix: Prefix for block file links (e.g., "block-integrations/")
|
||||
"""
|
||||
lines = []
|
||||
lines.append("# Table of contents")
|
||||
lines.append("")
|
||||
lines.append("* [AutoGPT Blocks Overview](README.md)")
|
||||
lines.append("")
|
||||
|
||||
# Check for guides/ directory at the root level (docs/integrations/guides/)
|
||||
guides_dir = root_dir / "guides"
|
||||
if guides_dir.exists():
|
||||
lines.append("## Guides")
|
||||
lines.append("")
|
||||
for guide_file in sorted(guides_dir.glob("*.md")):
|
||||
# Use just the file name for title (replace hyphens/underscores with spaces)
|
||||
title = file_path_to_title(guide_file.stem.replace("-", "_") + ".md")
|
||||
lines.append(f"* [{title}](guides/{guide_file.name})")
|
||||
lines.append("")
|
||||
|
||||
lines.append("## Block Integrations")
|
||||
lines.append("")
|
||||
|
||||
file_mapping = get_block_file_mapping(blocks)
|
||||
for file_path in sorted(file_mapping.keys()):
|
||||
title = file_path_to_title(file_path)
|
||||
link_path = f"{block_dir_prefix}{file_path}"
|
||||
lines.append(f"* [{title}]({link_path})")
|
||||
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def load_all_blocks_for_docs() -> list[BlockDoc]:
|
||||
"""Load all blocks and extract documentation."""
|
||||
from backend.blocks import load_all_blocks
|
||||
@@ -653,6 +734,16 @@ def write_block_docs(
|
||||
)
|
||||
)
|
||||
|
||||
# Add file-level additional_content section if present
|
||||
file_additional = extract_manual_content(existing_content).get(
|
||||
"additional_content", ""
|
||||
)
|
||||
if file_additional:
|
||||
content_parts.append("<!-- MANUAL: additional_content -->")
|
||||
content_parts.append(file_additional)
|
||||
content_parts.append("<!-- END MANUAL -->")
|
||||
content_parts.append("")
|
||||
|
||||
full_content = file_header + "\n" + "\n".join(content_parts)
|
||||
generated_files[str(file_path)] = full_content
|
||||
|
||||
@@ -661,14 +752,28 @@ def write_block_docs(
|
||||
|
||||
full_path.write_text(full_content)
|
||||
|
||||
# Generate overview file
|
||||
overview_content = generate_overview_table(blocks)
|
||||
overview_path = output_dir / "README.md"
|
||||
# Generate overview file at the parent directory (docs/integrations/)
|
||||
# with links prefixed to point into block-integrations/
|
||||
root_dir = output_dir.parent
|
||||
block_dir_name = output_dir.name # "block-integrations"
|
||||
block_dir_prefix = f"{block_dir_name}/"
|
||||
|
||||
overview_content = generate_overview_table(blocks, block_dir_prefix)
|
||||
overview_path = root_dir / "README.md"
|
||||
generated_files["README.md"] = overview_content
|
||||
overview_path.write_text(overview_content)
|
||||
|
||||
if verbose:
|
||||
print(" Writing README.md (overview)")
|
||||
print(" Writing README.md (overview) to parent directory")
|
||||
|
||||
# Generate SUMMARY.md for GitBook navigation at the parent directory
|
||||
summary_content = generate_summary_md(blocks, root_dir, block_dir_prefix)
|
||||
summary_path = root_dir / "SUMMARY.md"
|
||||
generated_files["SUMMARY.md"] = summary_content
|
||||
summary_path.write_text(summary_content)
|
||||
|
||||
if verbose:
|
||||
print(" Writing SUMMARY.md (navigation) to parent directory")
|
||||
|
||||
return generated_files
|
||||
|
||||
@@ -748,6 +853,16 @@ def check_docs_in_sync(output_dir: Path, blocks: list[BlockDoc]) -> bool:
|
||||
elif block_match.group(1).strip() != expected_block_content.strip():
|
||||
mismatched_blocks.append(block.name)
|
||||
|
||||
# Add file-level additional_content to expected content (matches write_block_docs)
|
||||
file_additional = extract_manual_content(existing_content).get(
|
||||
"additional_content", ""
|
||||
)
|
||||
if file_additional:
|
||||
content_parts.append("<!-- MANUAL: additional_content -->")
|
||||
content_parts.append(file_additional)
|
||||
content_parts.append("<!-- END MANUAL -->")
|
||||
content_parts.append("")
|
||||
|
||||
expected_content = file_header + "\n" + "\n".join(content_parts)
|
||||
|
||||
if existing_content.strip() != expected_content.strip():
|
||||
@@ -757,11 +872,15 @@ def check_docs_in_sync(output_dir: Path, blocks: list[BlockDoc]) -> bool:
|
||||
out_of_sync_details.append((file_path, mismatched_blocks))
|
||||
all_match = False
|
||||
|
||||
# Check overview
|
||||
overview_path = output_dir / "README.md"
|
||||
# Check overview at the parent directory (docs/integrations/)
|
||||
root_dir = output_dir.parent
|
||||
block_dir_name = output_dir.name # "block-integrations"
|
||||
block_dir_prefix = f"{block_dir_name}/"
|
||||
|
||||
overview_path = root_dir / "README.md"
|
||||
if overview_path.exists():
|
||||
existing_overview = overview_path.read_text()
|
||||
expected_overview = generate_overview_table(blocks)
|
||||
expected_overview = generate_overview_table(blocks, block_dir_prefix)
|
||||
if existing_overview.strip() != expected_overview.strip():
|
||||
print("OUT OF SYNC: README.md (overview)")
|
||||
print(" The blocks overview table needs regeneration")
|
||||
@@ -772,6 +891,21 @@ def check_docs_in_sync(output_dir: Path, blocks: list[BlockDoc]) -> bool:
|
||||
out_of_sync_details.append(("README.md", ["overview table"]))
|
||||
all_match = False
|
||||
|
||||
# Check SUMMARY.md at the parent directory
|
||||
summary_path = root_dir / "SUMMARY.md"
|
||||
if summary_path.exists():
|
||||
existing_summary = summary_path.read_text()
|
||||
expected_summary = generate_summary_md(blocks, root_dir, block_dir_prefix)
|
||||
if existing_summary.strip() != expected_summary.strip():
|
||||
print("OUT OF SYNC: SUMMARY.md (navigation)")
|
||||
print(" The GitBook navigation needs regeneration")
|
||||
out_of_sync_details.append(("SUMMARY.md", ["navigation"]))
|
||||
all_match = False
|
||||
else:
|
||||
print("MISSING: SUMMARY.md (navigation)")
|
||||
out_of_sync_details.append(("SUMMARY.md", ["navigation"]))
|
||||
all_match = False
|
||||
|
||||
# Check for unfilled manual sections
|
||||
unfilled_patterns = [
|
||||
"_Add a description of this category of blocks._",
|
||||
|
||||
@@ -120,6 +120,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@chromatic-com/storybook": "4.1.2",
|
||||
"happy-dom": "20.3.4",
|
||||
"@opentelemetry/instrumentation": "0.209.0",
|
||||
"@playwright/test": "1.56.1",
|
||||
"@storybook/addon-a11y": "9.1.5",
|
||||
@@ -130,7 +131,6 @@
|
||||
"@tanstack/eslint-plugin-query": "5.91.2",
|
||||
"@tanstack/react-query-devtools": "5.90.2",
|
||||
"@testing-library/dom": "10.4.1",
|
||||
"@testing-library/jest-dom": "6.9.1",
|
||||
"@testing-library/react": "16.3.2",
|
||||
"@types/canvas-confetti": "1.9.0",
|
||||
"@types/lodash": "4.17.20",
|
||||
@@ -148,7 +148,6 @@
|
||||
"eslint": "8.57.1",
|
||||
"eslint-config-next": "15.5.7",
|
||||
"eslint-plugin-storybook": "9.1.5",
|
||||
"happy-dom": "20.3.4",
|
||||
"import-in-the-middle": "2.0.2",
|
||||
"msw": "2.11.6",
|
||||
"msw-storybook-addon": "2.0.6",
|
||||
|
||||
3
autogpt_platform/frontend/pnpm-lock.yaml
generated
@@ -306,9 +306,6 @@ importers:
|
||||
'@testing-library/dom':
|
||||
specifier: 10.4.1
|
||||
version: 10.4.1
|
||||
'@testing-library/jest-dom':
|
||||
specifier: 6.9.1
|
||||
version: 6.9.1
|
||||
'@testing-library/react':
|
||||
specifier: 16.3.2
|
||||
version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@18.3.5(@types/react@18.3.17))(@types/react@18.3.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
|
||||
@@ -80,7 +80,6 @@ export const AgentInfo = ({
|
||||
const allVersions = storeData?.versions
|
||||
? storeData.versions
|
||||
.map((versionStr: string) => parseInt(versionStr, 10))
|
||||
.filter((versionNum: number) => !isNaN(versionNum))
|
||||
.sort((a: number, b: number) => b - a)
|
||||
.map((versionNum: number) => ({
|
||||
version: versionNum,
|
||||
|
||||
@@ -1,173 +0,0 @@
|
||||
import { describe, expect, test, afterEach } from "vitest";
|
||||
import {
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
act,
|
||||
} from "@/tests/integrations/test-utils";
|
||||
import { MainAgentPage } from "../MainAgentPage";
|
||||
import { server } from "@/mocks/mock-server";
|
||||
import { getGetV2GetSpecificAgentMockHandler422 } from "@/app/api/__generated__/endpoints/store/store.msw";
|
||||
import { create500Handler } from "@/tests/integrations/helpers/create-500-handler";
|
||||
import {
|
||||
mockAuthenticatedUser,
|
||||
mockUnauthenticatedUser,
|
||||
resetAuthState,
|
||||
} from "@/tests/integrations/helpers/mock-supabase-auth";
|
||||
|
||||
const defaultParams = {
|
||||
creator: "test-creator",
|
||||
slug: "test-agent",
|
||||
};
|
||||
|
||||
describe("MainAgentPage", () => {
|
||||
afterEach(() => {
|
||||
resetAuthState();
|
||||
});
|
||||
|
||||
describe("rendering", () => {
|
||||
test("renders agent info with title", async () => {
|
||||
render(<MainAgentPage params={defaultParams} />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("agent-title")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test("renders agent creator info", async () => {
|
||||
render(<MainAgentPage params={defaultParams} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("agent-creator")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test("renders agent description", async () => {
|
||||
render(<MainAgentPage params={defaultParams} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("agent-description")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test("renders breadcrumbs with marketplace link", async () => {
|
||||
render(<MainAgentPage params={defaultParams} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByRole("link", { name: /marketplace/i }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test("renders download button", async () => {
|
||||
render(<MainAgentPage params={defaultParams} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("agent-download-button")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test("renders similar agents section", async () => {
|
||||
render(<MainAgentPage params={defaultParams} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText("Similar agents", { exact: false }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("auth state", () => {
|
||||
test("shows add to library button when authenticated", async () => {
|
||||
mockAuthenticatedUser();
|
||||
render(<MainAgentPage params={defaultParams} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByTestId("agent-add-library-button"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test("hides add to library button when not authenticated", async () => {
|
||||
mockUnauthenticatedUser();
|
||||
render(<MainAgentPage params={defaultParams} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("agent-title")).toBeInTheDocument();
|
||||
});
|
||||
expect(
|
||||
screen.queryByTestId("agent-add-library-button"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders page correctly when logged out", async () => {
|
||||
mockUnauthenticatedUser();
|
||||
render(<MainAgentPage params={defaultParams} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("agent-title")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByTestId("agent-download-button")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders page correctly when logged in", async () => {
|
||||
mockAuthenticatedUser();
|
||||
render(<MainAgentPage params={defaultParams} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("agent-title")).toBeInTheDocument();
|
||||
});
|
||||
expect(
|
||||
screen.getByTestId("agent-add-library-button"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("error handling", () => {
|
||||
test("displays error when agent API returns 422", async () => {
|
||||
server.use(getGetV2GetSpecificAgentMockHandler422());
|
||||
|
||||
render(<MainAgentPage params={defaultParams} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText("Failed to load agent data", { exact: false }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await act(async () => {});
|
||||
});
|
||||
|
||||
test("displays error when API returns 500", async () => {
|
||||
server.use(
|
||||
create500Handler("get", "*/api/store/agents/test-creator/test-agent"),
|
||||
);
|
||||
|
||||
render(<MainAgentPage params={defaultParams} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText("Failed to load agent data", { exact: false }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await act(async () => {});
|
||||
});
|
||||
|
||||
test("retry button is visible on error", async () => {
|
||||
server.use(getGetV2GetSpecificAgentMockHandler422());
|
||||
|
||||
render(<MainAgentPage params={defaultParams} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByRole("button", { name: /try again/i }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await act(async () => {});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,131 +0,0 @@
|
||||
import { describe, expect, test, afterEach } from "vitest";
|
||||
import { cleanup, render, screen, waitFor } from "@/tests/integrations/test-utils";
|
||||
import { MainCreatorPage } from "../MainCreatorPage";
|
||||
import { server } from "@/mocks/mock-server";
|
||||
import {
|
||||
getGetV2GetCreatorDetailsMockHandler422,
|
||||
getGetV2ListStoreAgentsMockHandler422,
|
||||
} from "@/app/api/__generated__/endpoints/store/store.msw";
|
||||
import { create500Handler } from "@/tests/integrations/helpers/create-500-handler";
|
||||
import {
|
||||
mockAuthenticatedUser,
|
||||
mockUnauthenticatedUser,
|
||||
resetAuthState,
|
||||
} from "@/tests/integrations/helpers/mock-supabase-auth";
|
||||
|
||||
const defaultParams = {
|
||||
creator: "test-creator",
|
||||
};
|
||||
|
||||
describe("MainCreatorPage", () => {
|
||||
afterEach(() => {
|
||||
resetAuthState();
|
||||
});
|
||||
|
||||
describe("rendering", () => {
|
||||
test("renders creator info card", async () => {
|
||||
render(<MainCreatorPage params={defaultParams} />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("creator-description")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test("renders breadcrumbs with marketplace link", async () => {
|
||||
render(<MainCreatorPage params={defaultParams} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByRole("link", { name: /marketplace/i }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test("renders about section", async () => {
|
||||
render(<MainCreatorPage params={defaultParams} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("About")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test("renders agents by creator section", async () => {
|
||||
render(<MainCreatorPage params={defaultParams} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText(/Agents by/i, { exact: false }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("auth state", () => {
|
||||
test("renders page correctly when logged out", async () => {
|
||||
mockUnauthenticatedUser();
|
||||
render(<MainCreatorPage params={defaultParams} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("creator-description")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test("renders page correctly when logged in", async () => {
|
||||
mockAuthenticatedUser();
|
||||
render(<MainCreatorPage params={defaultParams} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("creator-description")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("error handling", () => {
|
||||
test("displays error when creator details API returns 422", async () => {
|
||||
server.use(getGetV2GetCreatorDetailsMockHandler422());
|
||||
|
||||
render(<MainCreatorPage params={defaultParams} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText("Failed to load creator data", { exact: false }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test("displays error when creator agents API returns 422", async () => {
|
||||
server.use(getGetV2ListStoreAgentsMockHandler422());
|
||||
|
||||
render(<MainCreatorPage params={defaultParams} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText("Failed to load creator data", { exact: false }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test("displays error when API returns 500", async () => {
|
||||
server.use(create500Handler("get", "*/api/store/creator/test-creator"));
|
||||
|
||||
render(<MainCreatorPage params={defaultParams} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText("Failed to load creator data", { exact: false }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test("retry button is visible on error", async () => {
|
||||
server.use(getGetV2GetCreatorDetailsMockHandler422());
|
||||
|
||||
render(<MainCreatorPage params={defaultParams} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByRole("button", { name: /try again/i }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,132 +1,15 @@
|
||||
import { describe, expect, test, afterEach } from "vitest";
|
||||
import { render, screen, waitFor } from "@/tests/integrations/test-utils";
|
||||
import { expect, test } from "vitest";
|
||||
import { render, screen } from "@/tests/integrations/test-utils";
|
||||
import { MainMarkeplacePage } from "../MainMarketplacePage";
|
||||
import { server } from "@/mocks/mock-server";
|
||||
import {
|
||||
getGetV2ListStoreAgentsMockHandler422,
|
||||
getGetV2ListStoreCreatorsMockHandler422,
|
||||
} from "@/app/api/__generated__/endpoints/store/store.msw";
|
||||
import { create500Handler } from "@/tests/integrations/helpers/create-500-handler";
|
||||
import {
|
||||
mockAuthenticatedUser,
|
||||
mockUnauthenticatedUser,
|
||||
resetAuthState,
|
||||
} from "@/tests/integrations/helpers/mock-supabase-auth";
|
||||
import { getDeleteV2DeleteStoreSubmissionMockHandler422 } from "@/app/api/__generated__/endpoints/store/store.msw";
|
||||
|
||||
describe("MainMarketplacePage", () => {
|
||||
afterEach(() => {
|
||||
resetAuthState();
|
||||
});
|
||||
// Only for CI testing purpose, will remove it in future PR
|
||||
test("MainMarketplacePage", async () => {
|
||||
server.use(getDeleteV2DeleteStoreSubmissionMockHandler422());
|
||||
|
||||
describe("rendering", () => {
|
||||
test("renders hero section with search bar", async () => {
|
||||
render(<MainMarkeplacePage />);
|
||||
|
||||
expect(
|
||||
await screen.findByText("Featured agents", { exact: false }),
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByPlaceholderText(/search/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders featured agents section", async () => {
|
||||
render(<MainMarkeplacePage />);
|
||||
|
||||
expect(
|
||||
await screen.findByText("Featured agents", { exact: false }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders top agents section", async () => {
|
||||
render(<MainMarkeplacePage />);
|
||||
|
||||
expect(
|
||||
await screen.findByText("Top Agents", { exact: false }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders featured creators section", async () => {
|
||||
render(<MainMarkeplacePage />);
|
||||
|
||||
expect(
|
||||
await screen.findByText("Featured creators", { exact: false }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("auth state", () => {
|
||||
test("renders page correctly when logged out", async () => {
|
||||
mockUnauthenticatedUser();
|
||||
render(<MainMarkeplacePage />);
|
||||
|
||||
expect(
|
||||
await screen.findByText("Featured agents", { exact: false }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("Top Agents", { exact: false }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders page correctly when logged in", async () => {
|
||||
mockAuthenticatedUser();
|
||||
render(<MainMarkeplacePage />);
|
||||
|
||||
expect(
|
||||
await screen.findByText("Featured agents", { exact: false }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("Top Agents", { exact: false }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("error handling", () => {
|
||||
test("displays error when featured agents API returns 422", async () => {
|
||||
server.use(getGetV2ListStoreAgentsMockHandler422());
|
||||
|
||||
render(<MainMarkeplacePage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText("Failed to load marketplace data", { exact: false }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test("displays error when creators API returns 422", async () => {
|
||||
server.use(getGetV2ListStoreCreatorsMockHandler422());
|
||||
|
||||
render(<MainMarkeplacePage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText("Failed to load marketplace data", { exact: false }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test("displays error when API returns 500", async () => {
|
||||
server.use(create500Handler("get", "*/api/store/agents*"));
|
||||
|
||||
render(<MainMarkeplacePage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText("Failed to load marketplace data", { exact: false }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test("retry button is visible on error", async () => {
|
||||
server.use(getGetV2ListStoreAgentsMockHandler422());
|
||||
|
||||
render(<MainMarkeplacePage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByRole("button", { name: /try again/i }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
render(<MainMarkeplacePage />);
|
||||
expect(
|
||||
await screen.findByText("Featured agents", { exact: false }),
|
||||
).toBeDefined();
|
||||
});
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
import { describe, expect, test, afterEach } from "vitest";
|
||||
import { render, screen, waitFor } from "@/tests/integrations/test-utils";
|
||||
import { MainSearchResultPage } from "../MainSearchResultPage";
|
||||
import { server } from "@/mocks/mock-server";
|
||||
import {
|
||||
getGetV2ListStoreAgentsMockHandler422,
|
||||
getGetV2ListStoreCreatorsMockHandler422,
|
||||
} from "@/app/api/__generated__/endpoints/store/store.msw";
|
||||
import { create500Handler } from "@/tests/integrations/helpers/create-500-handler";
|
||||
import {
|
||||
mockAuthenticatedUser,
|
||||
mockUnauthenticatedUser,
|
||||
resetAuthState,
|
||||
} from "@/tests/integrations/helpers/mock-supabase-auth";
|
||||
|
||||
const defaultProps = {
|
||||
searchTerm: "test-search",
|
||||
sort: undefined as undefined,
|
||||
};
|
||||
|
||||
describe("MainSearchResultPage", () => {
|
||||
afterEach(() => {
|
||||
resetAuthState();
|
||||
});
|
||||
|
||||
describe("rendering", () => {
|
||||
test("renders search results header with search term", async () => {
|
||||
render(<MainSearchResultPage {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Results for:")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText("test-search")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders search bar", async () => {
|
||||
render(<MainSearchResultPage {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByPlaceholderText(/search/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("auth state", () => {
|
||||
test("renders page correctly when logged out", async () => {
|
||||
mockUnauthenticatedUser();
|
||||
render(<MainSearchResultPage {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Results for:")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test("renders page correctly when logged in", async () => {
|
||||
mockAuthenticatedUser();
|
||||
render(<MainSearchResultPage {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Results for:")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("error handling", () => {
|
||||
test("displays error when agents API returns 422", async () => {
|
||||
server.use(getGetV2ListStoreAgentsMockHandler422());
|
||||
|
||||
render(<MainSearchResultPage {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText("Failed to load marketplace data", { exact: false }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test("displays error when creators API returns 422", async () => {
|
||||
server.use(getGetV2ListStoreCreatorsMockHandler422());
|
||||
|
||||
render(<MainSearchResultPage {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText("Failed to load marketplace data", { exact: false }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test("displays error when API returns 500", async () => {
|
||||
server.use(create500Handler("get", "*/api/store/agents*"));
|
||||
|
||||
render(<MainSearchResultPage {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText("Failed to load marketplace data", { exact: false }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test("retry button is visible on error", async () => {
|
||||
server.use(getGetV2ListStoreAgentsMockHandler422());
|
||||
|
||||
render(<MainSearchResultPage {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByRole("button", { name: /try again/i }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -136,19 +136,16 @@ export const customMutator = async <
|
||||
response.statusText ||
|
||||
`HTTP ${response.status}`;
|
||||
|
||||
const isTestEnv = process.env.NODE_ENV === 'test';
|
||||
if (!isTestEnv) {
|
||||
console.error(
|
||||
`Request failed ${environment.isServerSide() ? "on server" : "on client"}`,
|
||||
{
|
||||
status: response.status,
|
||||
method,
|
||||
url: fullUrl.replace(baseUrl, ""), // Show relative URL for cleaner logs
|
||||
errorMessage,
|
||||
responseData: responseData || "No response data",
|
||||
},
|
||||
);
|
||||
}
|
||||
console.error(
|
||||
`Request failed ${environment.isServerSide() ? "on server" : "on client"}`,
|
||||
{
|
||||
status: response.status,
|
||||
method,
|
||||
url: fullUrl.replace(baseUrl, ""), // Show relative URL for cleaner logs
|
||||
errorMessage,
|
||||
responseData: responseData || "No response data",
|
||||
},
|
||||
);
|
||||
|
||||
throw new ApiError(errorMessage, response.status, responseData);
|
||||
}
|
||||
|
||||
@@ -218,61 +218,3 @@ test("shows error when deletion fails", async () => {
|
||||
4. **Co-locate integration tests** - Keep `__tests__/` folder next to the component
|
||||
5. **E2E is expensive** - Only for critical happy paths; prefer integration tests
|
||||
6. **AI agents are good at writing integration tests** - Start with these when adding test coverage
|
||||
|
||||
---
|
||||
|
||||
## Testing 500 Server Errors
|
||||
|
||||
Orval auto-generates 422 validation error handlers, but 500 errors must be created manually. Use the helper:
|
||||
|
||||
```tsx
|
||||
import { create500Handler } from "@/tests/integrations/helpers/create-500-handler";
|
||||
|
||||
test("handles server error", async () => {
|
||||
server.use(create500Handler("get", "*/api/store/agents"));
|
||||
render(<Component />);
|
||||
expect(await screen.findByText("Failed to load")).toBeInTheDocument();
|
||||
});
|
||||
```
|
||||
|
||||
Options:
|
||||
|
||||
- `delayMs`: Add delay before response (for testing loading states)
|
||||
- `body`: Custom error response body
|
||||
|
||||
---
|
||||
|
||||
## Testing Auth-Dependent Components
|
||||
|
||||
For components that behave differently based on login state:
|
||||
|
||||
```tsx
|
||||
import {
|
||||
mockAuthenticatedUser,
|
||||
mockUnauthenticatedUser,
|
||||
resetAuthState,
|
||||
} from "@/tests/integrations/helpers/mock-supabase-auth";
|
||||
|
||||
describe("MyComponent", () => {
|
||||
afterEach(() => {
|
||||
resetAuthState();
|
||||
});
|
||||
|
||||
test("shows feature when logged in", async () => {
|
||||
mockAuthenticatedUser();
|
||||
render(<MyComponent />);
|
||||
expect(await screen.findByText("Premium Feature")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("hides feature when logged out", async () => {
|
||||
mockUnauthenticatedUser();
|
||||
render(<MyComponent />);
|
||||
expect(screen.queryByText("Premium Feature")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("with custom user data", async () => {
|
||||
mockAuthenticatedUser({ email: "custom@test.com" });
|
||||
// ...
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import { http, HttpResponse, delay } from "msw";
|
||||
|
||||
type HttpMethod = "get" | "post" | "put" | "patch" | "delete";
|
||||
|
||||
interface Create500HandlerOptions {
|
||||
delayMs?: number;
|
||||
body?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export function create500Handler(
|
||||
method: HttpMethod,
|
||||
url: string,
|
||||
options?: Create500HandlerOptions,
|
||||
) {
|
||||
const { delayMs = 0, body } = options ?? {};
|
||||
|
||||
const responseBody = body ?? {
|
||||
detail: "Internal Server Error",
|
||||
};
|
||||
|
||||
return http[method](url, async () => {
|
||||
if (delayMs > 0) {
|
||||
await delay(delayMs);
|
||||
}
|
||||
|
||||
return HttpResponse.json(responseBody, {
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import { createContext, useContext, ReactNode } from "react";
|
||||
import { UserOnboarding } from "@/lib/autogpt-server-api";
|
||||
import { PostV1CompleteOnboardingStepStep } from "@/app/api/__generated__/models/postV1CompleteOnboardingStepStep";
|
||||
import type { LocalOnboardingStateUpdate } from "@/providers/onboarding/helpers";
|
||||
|
||||
const MockOnboardingContext = createContext<{
|
||||
state: UserOnboarding | null;
|
||||
updateState: (state: LocalOnboardingStateUpdate) => void;
|
||||
step: number;
|
||||
setStep: (step: number) => void;
|
||||
completeStep: (step: PostV1CompleteOnboardingStepStep) => void;
|
||||
}>({
|
||||
state: null,
|
||||
updateState: () => {},
|
||||
step: 1,
|
||||
setStep: () => {},
|
||||
completeStep: () => {},
|
||||
});
|
||||
|
||||
export function useOnboarding(
|
||||
step?: number,
|
||||
completeStep?: PostV1CompleteOnboardingStepStep,
|
||||
) {
|
||||
const context = useContext(MockOnboardingContext);
|
||||
return context;
|
||||
}
|
||||
|
||||
export function MockOnboardingProvider({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<MockOnboardingContext.Provider
|
||||
value={{
|
||||
state: null,
|
||||
updateState: () => {},
|
||||
step: 1,
|
||||
setStep: () => {},
|
||||
completeStep: () => {},
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</MockOnboardingContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import type { User } from "@supabase/supabase-js";
|
||||
import { useSupabaseStore } from "@/lib/supabase/hooks/useSupabaseStore";
|
||||
|
||||
export const mockUser: User = {
|
||||
id: "test-user-id",
|
||||
email: "test@example.com",
|
||||
aud: "authenticated",
|
||||
role: "authenticated",
|
||||
created_at: new Date().toISOString(),
|
||||
app_metadata: {},
|
||||
user_metadata: {},
|
||||
};
|
||||
|
||||
export function mockAuthenticatedUser(user: Partial<User> = {}): User {
|
||||
const mergedUser = { ...mockUser, ...user };
|
||||
|
||||
useSupabaseStore.setState({
|
||||
user: mergedUser,
|
||||
isUserLoading: false,
|
||||
hasLoadedUser: true,
|
||||
});
|
||||
|
||||
return mergedUser;
|
||||
}
|
||||
|
||||
export function mockUnauthenticatedUser(): void {
|
||||
useSupabaseStore.setState({
|
||||
user: null,
|
||||
isUserLoading: false,
|
||||
hasLoadedUser: true,
|
||||
});
|
||||
}
|
||||
|
||||
export function resetAuthState(): void {
|
||||
useSupabaseStore.setState({
|
||||
user: null,
|
||||
isUserLoading: true,
|
||||
hasLoadedUser: false,
|
||||
});
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
// Suppresses expected act(...) warnings from React Query and component async updates.
|
||||
// These warnings are normal behavior with React Query and don't indicate test failures.
|
||||
export function suppressReactQueryUpdateWarning() {
|
||||
const originalError = console.error;
|
||||
|
||||
console.error = (...args: unknown[]) => {
|
||||
const isActWarning = args.some(
|
||||
(arg) =>
|
||||
typeof arg === "string" &&
|
||||
(arg.includes("not wrapped in act(...)") ||
|
||||
arg.includes("An update to") && arg.includes("inside a test"))
|
||||
);
|
||||
|
||||
if (isActWarning) {
|
||||
const fullMessage = args
|
||||
.map((arg) => String(arg))
|
||||
.join("\n")
|
||||
.toLowerCase();
|
||||
|
||||
const isReactQueryRelated =
|
||||
fullMessage.includes("queryclientprovider") ||
|
||||
fullMessage.includes("react query") ||
|
||||
fullMessage.includes("@tanstack/react-query");
|
||||
|
||||
if (isReactQueryRelated || fullMessage.includes("testproviders")) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
originalError(...args);
|
||||
};
|
||||
|
||||
// Return cleanup function
|
||||
return () => {
|
||||
console.error = originalError;
|
||||
};
|
||||
}
|
||||
@@ -1,22 +1,14 @@
|
||||
import { BackendAPIProvider } from "@/lib/autogpt-server-api/context";
|
||||
import OnboardingProvider from "@/providers/onboarding/onboarding-provider";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { render, RenderOptions, act } from "@testing-library/react";
|
||||
import { render, RenderOptions } from "@testing-library/react";
|
||||
import { ReactElement, ReactNode } from "react";
|
||||
import { MockOnboardingProvider, useOnboarding as mockUseOnboarding } from "./helpers/mock-onboarding-provider";
|
||||
|
||||
vi.mock("@/providers/onboarding/onboarding-provider", () => ({
|
||||
useOnboarding: mockUseOnboarding,
|
||||
default: vi.fn(),
|
||||
}));
|
||||
|
||||
function createTestQueryClient() {
|
||||
return new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
refetchOnReconnect: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -27,7 +19,7 @@ function TestProviders({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<BackendAPIProvider>
|
||||
<MockOnboardingProvider>{children}</MockOnboardingProvider>
|
||||
<OnboardingProvider>{children}</OnboardingProvider>
|
||||
</BackendAPIProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
@@ -2,17 +2,11 @@ import { beforeAll, afterAll, afterEach } from "vitest";
|
||||
import { server } from "@/mocks/mock-server";
|
||||
import { mockNextjsModules } from "./setup-nextjs-mocks";
|
||||
import { mockSupabaseRequest } from "./mock-supabase-request";
|
||||
import "@testing-library/jest-dom";
|
||||
import { suppressReactQueryUpdateWarning } from "./helpers/supress-react-query-update-warning";
|
||||
|
||||
beforeAll(() => {
|
||||
mockNextjsModules();
|
||||
mockSupabaseRequest();
|
||||
const restoreConsoleError = suppressReactQueryUpdateWarning();
|
||||
afterAll(() => {
|
||||
restoreConsoleError();
|
||||
});
|
||||
mockSupabaseRequest(); // If you need user's data - please mock supabase actions in your specific test - it sends null user [It's only to avoid cookies() call]
|
||||
return server.listen({ onUnhandledRequest: "error" });
|
||||
});
|
||||
afterEach(() => {server.resetHandlers()});
|
||||
afterEach(() => server.resetHandlers());
|
||||
afterAll(() => server.close());
|
||||
|
||||
@@ -9,7 +9,78 @@ function escapeRegExp(value: string) {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
test.describe("Marketplace Agent Page - Cross-Page Flows", () => {
|
||||
test.describe("Marketplace Agent Page - Basic Functionality", () => {
|
||||
test("User can access agent page when logged out", async ({ page }) => {
|
||||
const marketplacePage = new MarketplacePage(page);
|
||||
|
||||
await marketplacePage.goto(page);
|
||||
await hasUrl(page, "/marketplace");
|
||||
|
||||
const firstStoreCard = await marketplacePage.getFirstTopAgent();
|
||||
await firstStoreCard.click();
|
||||
|
||||
await page.waitForURL("**/marketplace/agent/**");
|
||||
await matchesUrl(page, /\/marketplace\/agent\/.+/);
|
||||
});
|
||||
|
||||
test("User can access agent page when logged in", async ({ page }) => {
|
||||
const loginPage = new LoginPage(page);
|
||||
const marketplacePage = new MarketplacePage(page);
|
||||
|
||||
await loginPage.goto();
|
||||
const richUser = getTestUserWithLibraryAgents();
|
||||
await loginPage.login(richUser.email, richUser.password);
|
||||
await hasUrl(page, "/marketplace");
|
||||
await marketplacePage.goto(page);
|
||||
await hasUrl(page, "/marketplace");
|
||||
|
||||
const firstStoreCard = await marketplacePage.getFirstTopAgent();
|
||||
await firstStoreCard.click();
|
||||
|
||||
await page.waitForURL("**/marketplace/agent/**");
|
||||
await matchesUrl(page, /\/marketplace\/agent\/.+/);
|
||||
});
|
||||
|
||||
test("Agent page details are visible", async ({ page }) => {
|
||||
const { getId } = getSelectors(page);
|
||||
|
||||
const marketplacePage = new MarketplacePage(page);
|
||||
await marketplacePage.goto(page);
|
||||
|
||||
const firstStoreCard = await marketplacePage.getFirstTopAgent();
|
||||
await firstStoreCard.click();
|
||||
await page.waitForURL("**/marketplace/agent/**");
|
||||
|
||||
const agentTitle = getId("agent-title");
|
||||
await isVisible(agentTitle);
|
||||
|
||||
const agentDescription = getId("agent-description");
|
||||
await isVisible(agentDescription);
|
||||
|
||||
const creatorInfo = getId("agent-creator");
|
||||
await isVisible(creatorInfo);
|
||||
});
|
||||
|
||||
test("Download button functionality works", async ({ page }) => {
|
||||
const { getId, getText } = getSelectors(page);
|
||||
|
||||
const marketplacePage = new MarketplacePage(page);
|
||||
await marketplacePage.goto(page);
|
||||
|
||||
const firstStoreCard = await marketplacePage.getFirstTopAgent();
|
||||
await firstStoreCard.click();
|
||||
await page.waitForURL("**/marketplace/agent/**");
|
||||
|
||||
const downloadButton = getId("agent-download-button");
|
||||
await isVisible(downloadButton);
|
||||
await downloadButton.click();
|
||||
|
||||
const downloadSuccessMessage = getText(
|
||||
"Your agent has been successfully downloaded.",
|
||||
);
|
||||
await isVisible(downloadSuccessMessage);
|
||||
});
|
||||
|
||||
test("Add to library button works and agent appears in library", async ({
|
||||
page,
|
||||
}) => {
|
||||
|
||||
@@ -1,8 +1,64 @@
|
||||
import { test } from "@playwright/test";
|
||||
import { getTestUserWithLibraryAgents } from "./credentials";
|
||||
import { LoginPage } from "./pages/login.page";
|
||||
import { MarketplacePage } from "./pages/marketplace.page";
|
||||
import { hasUrl, matchesUrl } from "./utils/assertion";
|
||||
import { hasUrl, isVisible, matchesUrl } from "./utils/assertion";
|
||||
import { getSelectors } from "./utils/selectors";
|
||||
|
||||
test.describe("Marketplace Creator Page – Basic Functionality", () => {
|
||||
test("User can access creator's page when logged out", async ({ page }) => {
|
||||
const marketplacePage = new MarketplacePage(page);
|
||||
|
||||
await marketplacePage.goto(page);
|
||||
await hasUrl(page, "/marketplace");
|
||||
|
||||
const firstCreatorProfile =
|
||||
await marketplacePage.getFirstCreatorProfile(page);
|
||||
await firstCreatorProfile.click();
|
||||
|
||||
await page.waitForURL("**/marketplace/creator/**");
|
||||
await matchesUrl(page, /\/marketplace\/creator\/.+/);
|
||||
});
|
||||
|
||||
test("User can access creator's page when logged in", async ({ page }) => {
|
||||
const loginPage = new LoginPage(page);
|
||||
const marketplacePage = new MarketplacePage(page);
|
||||
|
||||
await loginPage.goto();
|
||||
const richUser = getTestUserWithLibraryAgents();
|
||||
await loginPage.login(richUser.email, richUser.password);
|
||||
await hasUrl(page, "/marketplace");
|
||||
|
||||
await marketplacePage.goto(page);
|
||||
await hasUrl(page, "/marketplace");
|
||||
|
||||
const firstCreatorProfile =
|
||||
await marketplacePage.getFirstCreatorProfile(page);
|
||||
await firstCreatorProfile.click();
|
||||
|
||||
await page.waitForURL("**/marketplace/creator/**");
|
||||
await matchesUrl(page, /\/marketplace\/creator\/.+/);
|
||||
});
|
||||
|
||||
test("Creator page details are visible", async ({ page }) => {
|
||||
const { getId } = getSelectors(page);
|
||||
const marketplacePage = new MarketplacePage(page);
|
||||
|
||||
await marketplacePage.goto(page);
|
||||
await hasUrl(page, "/marketplace");
|
||||
|
||||
const firstCreatorProfile =
|
||||
await marketplacePage.getFirstCreatorProfile(page);
|
||||
await firstCreatorProfile.click();
|
||||
await page.waitForURL("**/marketplace/creator/**");
|
||||
|
||||
const creatorTitle = getId("creator-title");
|
||||
await isVisible(creatorTitle);
|
||||
|
||||
const creatorDescription = getId("creator-description");
|
||||
await isVisible(creatorDescription);
|
||||
});
|
||||
|
||||
test.describe("Marketplace Creator Page – Cross-Page Flows", () => {
|
||||
test("Agents in agent by sections navigation works", async ({ page }) => {
|
||||
const marketplacePage = new MarketplacePage(page);
|
||||
|
||||
|
||||
@@ -1,8 +1,74 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { getTestUserWithLibraryAgents } from "./credentials";
|
||||
import { LoginPage } from "./pages/login.page";
|
||||
import { MarketplacePage } from "./pages/marketplace.page";
|
||||
import { isVisible, matchesUrl } from "./utils/assertion";
|
||||
import { hasMinCount, hasUrl, isVisible, matchesUrl } from "./utils/assertion";
|
||||
|
||||
// Marketplace tests for store agent search functionality
|
||||
test.describe("Marketplace – Basic Functionality", () => {
|
||||
test("User can access marketplace page when logged out", async ({ page }) => {
|
||||
const marketplacePage = new MarketplacePage(page);
|
||||
|
||||
await marketplacePage.goto(page);
|
||||
await hasUrl(page, "/marketplace");
|
||||
|
||||
const marketplaceTitle = await marketplacePage.getMarketplaceTitle(page);
|
||||
await isVisible(marketplaceTitle);
|
||||
|
||||
console.log(
|
||||
"User can access marketplace page when logged out test passed ✅",
|
||||
);
|
||||
});
|
||||
|
||||
test("User can access marketplace page when logged in", async ({ page }) => {
|
||||
const loginPage = new LoginPage(page);
|
||||
const marketplacePage = new MarketplacePage(page);
|
||||
|
||||
await loginPage.goto();
|
||||
const richUser = getTestUserWithLibraryAgents();
|
||||
await loginPage.login(richUser.email, richUser.password);
|
||||
await hasUrl(page, "/marketplace");
|
||||
|
||||
await marketplacePage.goto(page);
|
||||
await hasUrl(page, "/marketplace");
|
||||
|
||||
const marketplaceTitle = await marketplacePage.getMarketplaceTitle(page);
|
||||
await isVisible(marketplaceTitle);
|
||||
|
||||
console.log(
|
||||
"User can access marketplace page when logged in test passed ✅",
|
||||
);
|
||||
});
|
||||
|
||||
test("Featured agents, top agents, and featured creators are visible", async ({
|
||||
page,
|
||||
}) => {
|
||||
const marketplacePage = new MarketplacePage(page);
|
||||
await marketplacePage.goto(page);
|
||||
|
||||
const featuredAgentsSection =
|
||||
await marketplacePage.getFeaturedAgentsSection(page);
|
||||
await isVisible(featuredAgentsSection);
|
||||
const featuredAgentCards =
|
||||
await marketplacePage.getFeaturedAgentCards(page);
|
||||
await hasMinCount(featuredAgentCards, 1);
|
||||
|
||||
const topAgentsSection = await marketplacePage.getTopAgentsSection(page);
|
||||
await isVisible(topAgentsSection);
|
||||
const topAgentCards = await marketplacePage.getTopAgentCards(page);
|
||||
await hasMinCount(topAgentCards, 1);
|
||||
|
||||
const featuredCreatorsSection =
|
||||
await marketplacePage.getFeaturedCreatorsSection(page);
|
||||
await isVisible(featuredCreatorsSection);
|
||||
const creatorProfiles = await marketplacePage.getCreatorProfiles(page);
|
||||
await hasMinCount(creatorProfiles, 1);
|
||||
|
||||
console.log(
|
||||
"Featured agents, top agents, and featured creators are visible test passed ✅",
|
||||
);
|
||||
});
|
||||
|
||||
test.describe("Marketplace – Navigation", () => {
|
||||
test("Can navigate and interact with marketplace elements", async ({
|
||||
page,
|
||||
}) => {
|
||||
@@ -30,7 +96,7 @@ test.describe("Marketplace – Navigation", () => {
|
||||
await matchesUrl(page, /\/marketplace\/creator\/.+/);
|
||||
|
||||
console.log(
|
||||
"Can navigate and interact with marketplace elements test passed",
|
||||
"Can navigate and interact with marketplace elements test passed ✅",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -55,6 +121,32 @@ test.describe("Marketplace – Navigation", () => {
|
||||
const results = await marketplacePage.getSearchResultsCount(page);
|
||||
expect(results).toBeGreaterThan(0);
|
||||
|
||||
console.log("Complete search flow works correctly test passed");
|
||||
console.log("Complete search flow works correctly test passed ✅");
|
||||
});
|
||||
|
||||
// We need to add a test search with filters, but the current business logic for filters doesn't work as expected. We'll add it once we modify that.
|
||||
});
|
||||
|
||||
test.describe("Marketplace – Edge Cases", () => {
|
||||
test("Search for non-existent item shows no results", async ({ page }) => {
|
||||
const marketplacePage = new MarketplacePage(page);
|
||||
await marketplacePage.goto(page);
|
||||
|
||||
await marketplacePage.searchAndNavigate("xyznonexistentitemxyz123", page);
|
||||
|
||||
await marketplacePage.waitForSearchResults();
|
||||
|
||||
await matchesUrl(page, /\/marketplace\/search\?searchTerm=/);
|
||||
|
||||
const resultsHeading = page.getByText("Results for:");
|
||||
await isVisible(resultsHeading);
|
||||
|
||||
const searchTerm = page.getByText("xyznonexistentitemxyz123");
|
||||
await isVisible(searchTerm);
|
||||
|
||||
const results = await marketplacePage.getSearchResultsCount(page);
|
||||
expect(results).toBe(0);
|
||||
|
||||
console.log("Search for non-existent item shows no results test passed ✅");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [{ "name": "next" }],
|
||||
"types": ["vitest/globals", "@testing-library/jest-dom/vitest"],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
|
||||
@@ -8,6 +8,5 @@ export default defineConfig({
|
||||
environment: "happy-dom",
|
||||
include: ["src/**/*.test.tsx"],
|
||||
setupFiles: ["./src/tests/integrations/vitest.setup.tsx"],
|
||||
globals: true,
|
||||
},
|
||||
});
|
||||
|
||||
BIN
docs/integrations/.gitbook/assets/Ollama-Add-Prompts.png
Normal file
|
After Width: | Height: | Size: 115 KiB |
BIN
docs/integrations/.gitbook/assets/Ollama-Output.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
docs/integrations/.gitbook/assets/Ollama-Remote-Host.png
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
BIN
docs/integrations/.gitbook/assets/Ollama-Select-Llama32.png
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
docs/integrations/.gitbook/assets/Select-AI-block.png
Normal file
|
After Width: | Height: | Size: 116 KiB |
BIN
docs/integrations/.gitbook/assets/e2b-dashboard.png
Normal file
|
After Width: | Height: | Size: 504 KiB |
BIN
docs/integrations/.gitbook/assets/e2b-log-url.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
docs/integrations/.gitbook/assets/e2b-new-tag.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
docs/integrations/.gitbook/assets/e2b-tag-button.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
docs/integrations/.gitbook/assets/get-repo-dialog.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
133
docs/integrations/SUMMARY.md
Normal file
@@ -0,0 +1,133 @@
|
||||
# Table of contents
|
||||
|
||||
* [AutoGPT Blocks Overview](README.md)
|
||||
|
||||
## Guides
|
||||
|
||||
* [LLM Providers](guides/llm-providers.md)
|
||||
* [Voice Providers](guides/voice-providers.md)
|
||||
|
||||
## Block Integrations
|
||||
|
||||
* [Airtable Bases](block-integrations/airtable/bases.md)
|
||||
* [Airtable Records](block-integrations/airtable/records.md)
|
||||
* [Airtable Schema](block-integrations/airtable/schema.md)
|
||||
* [Airtable Triggers](block-integrations/airtable/triggers.md)
|
||||
* [Apollo Organization](block-integrations/apollo/organization.md)
|
||||
* [Apollo People](block-integrations/apollo/people.md)
|
||||
* [Apollo Person](block-integrations/apollo/person.md)
|
||||
* [Ayrshare Post To Bluesky](block-integrations/ayrshare/post_to_bluesky.md)
|
||||
* [Ayrshare Post To Facebook](block-integrations/ayrshare/post_to_facebook.md)
|
||||
* [Ayrshare Post To GMB](block-integrations/ayrshare/post_to_gmb.md)
|
||||
* [Ayrshare Post To Instagram](block-integrations/ayrshare/post_to_instagram.md)
|
||||
* [Ayrshare Post To LinkedIn](block-integrations/ayrshare/post_to_linkedin.md)
|
||||
* [Ayrshare Post To Pinterest](block-integrations/ayrshare/post_to_pinterest.md)
|
||||
* [Ayrshare Post To Reddit](block-integrations/ayrshare/post_to_reddit.md)
|
||||
* [Ayrshare Post To Snapchat](block-integrations/ayrshare/post_to_snapchat.md)
|
||||
* [Ayrshare Post To Telegram](block-integrations/ayrshare/post_to_telegram.md)
|
||||
* [Ayrshare Post To Threads](block-integrations/ayrshare/post_to_threads.md)
|
||||
* [Ayrshare Post To TikTok](block-integrations/ayrshare/post_to_tiktok.md)
|
||||
* [Ayrshare Post To X](block-integrations/ayrshare/post_to_x.md)
|
||||
* [Ayrshare Post To YouTube](block-integrations/ayrshare/post_to_youtube.md)
|
||||
* [Baas Bots](block-integrations/baas/bots.md)
|
||||
* [Bannerbear Text Overlay](block-integrations/bannerbear/text_overlay.md)
|
||||
* [Basic](block-integrations/basic.md)
|
||||
* [Compass Triggers](block-integrations/compass/triggers.md)
|
||||
* [Data](block-integrations/data.md)
|
||||
* [Dataforseo Keyword Suggestions](block-integrations/dataforseo/keyword_suggestions.md)
|
||||
* [Dataforseo Related Keywords](block-integrations/dataforseo/related_keywords.md)
|
||||
* [Discord Bot Blocks](block-integrations/discord/bot_blocks.md)
|
||||
* [Discord OAuth Blocks](block-integrations/discord/oauth_blocks.md)
|
||||
* [Enrichlayer LinkedIn](block-integrations/enrichlayer/linkedin.md)
|
||||
* [Exa Answers](block-integrations/exa/answers.md)
|
||||
* [Exa Code Context](block-integrations/exa/code_context.md)
|
||||
* [Exa Contents](block-integrations/exa/contents.md)
|
||||
* [Exa Research](block-integrations/exa/research.md)
|
||||
* [Exa Search](block-integrations/exa/search.md)
|
||||
* [Exa Similar](block-integrations/exa/similar.md)
|
||||
* [Exa Webhook Blocks](block-integrations/exa/webhook_blocks.md)
|
||||
* [Exa Websets](block-integrations/exa/websets.md)
|
||||
* [Exa Websets Enrichment](block-integrations/exa/websets_enrichment.md)
|
||||
* [Exa Websets Import Export](block-integrations/exa/websets_import_export.md)
|
||||
* [Exa Websets Items](block-integrations/exa/websets_items.md)
|
||||
* [Exa Websets Monitor](block-integrations/exa/websets_monitor.md)
|
||||
* [Exa Websets Polling](block-integrations/exa/websets_polling.md)
|
||||
* [Exa Websets Search](block-integrations/exa/websets_search.md)
|
||||
* [Fal AI Video Generator](block-integrations/fal/ai_video_generator.md)
|
||||
* [Firecrawl Crawl](block-integrations/firecrawl/crawl.md)
|
||||
* [Firecrawl Extract](block-integrations/firecrawl/extract.md)
|
||||
* [Firecrawl Map](block-integrations/firecrawl/map.md)
|
||||
* [Firecrawl Scrape](block-integrations/firecrawl/scrape.md)
|
||||
* [Firecrawl Search](block-integrations/firecrawl/search.md)
|
||||
* [Generic Webhook Triggers](block-integrations/generic_webhook/triggers.md)
|
||||
* [GitHub Checks](block-integrations/github/checks.md)
|
||||
* [GitHub CI](block-integrations/github/ci.md)
|
||||
* [GitHub Issues](block-integrations/github/issues.md)
|
||||
* [GitHub Pull Requests](block-integrations/github/pull_requests.md)
|
||||
* [GitHub Repo](block-integrations/github/repo.md)
|
||||
* [GitHub Reviews](block-integrations/github/reviews.md)
|
||||
* [GitHub Statuses](block-integrations/github/statuses.md)
|
||||
* [GitHub Triggers](block-integrations/github/triggers.md)
|
||||
* [Google Calendar](block-integrations/google/calendar.md)
|
||||
* [Google Docs](block-integrations/google/docs.md)
|
||||
* [Google Gmail](block-integrations/google/gmail.md)
|
||||
* [Google Sheets](block-integrations/google/sheets.md)
|
||||
* [HubSpot Company](block-integrations/hubspot/company.md)
|
||||
* [HubSpot Contact](block-integrations/hubspot/contact.md)
|
||||
* [HubSpot Engagement](block-integrations/hubspot/engagement.md)
|
||||
* [Jina Chunking](block-integrations/jina/chunking.md)
|
||||
* [Jina Embeddings](block-integrations/jina/embeddings.md)
|
||||
* [Jina Fact Checker](block-integrations/jina/fact_checker.md)
|
||||
* [Jina Search](block-integrations/jina/search.md)
|
||||
* [Linear Comment](block-integrations/linear/comment.md)
|
||||
* [Linear Issues](block-integrations/linear/issues.md)
|
||||
* [Linear Projects](block-integrations/linear/projects.md)
|
||||
* [LLM](block-integrations/llm.md)
|
||||
* [Logic](block-integrations/logic.md)
|
||||
* [Misc](block-integrations/misc.md)
|
||||
* [Multimedia](block-integrations/multimedia.md)
|
||||
* [Notion Create Page](block-integrations/notion/create_page.md)
|
||||
* [Notion Read Database](block-integrations/notion/read_database.md)
|
||||
* [Notion Read Page](block-integrations/notion/read_page.md)
|
||||
* [Notion Read Page Markdown](block-integrations/notion/read_page_markdown.md)
|
||||
* [Notion Search](block-integrations/notion/search.md)
|
||||
* [Nvidia Deepfake](block-integrations/nvidia/deepfake.md)
|
||||
* [Replicate Flux Advanced](block-integrations/replicate/flux_advanced.md)
|
||||
* [Replicate Replicate Block](block-integrations/replicate/replicate_block.md)
|
||||
* [Search](block-integrations/search.md)
|
||||
* [Slant3D Filament](block-integrations/slant3d/filament.md)
|
||||
* [Slant3D Order](block-integrations/slant3d/order.md)
|
||||
* [Slant3D Slicing](block-integrations/slant3d/slicing.md)
|
||||
* [Slant3D Webhook](block-integrations/slant3d/webhook.md)
|
||||
* [Smartlead Campaign](block-integrations/smartlead/campaign.md)
|
||||
* [Stagehand Blocks](block-integrations/stagehand/blocks.md)
|
||||
* [System Library Operations](block-integrations/system/library_operations.md)
|
||||
* [System Store Operations](block-integrations/system/store_operations.md)
|
||||
* [Text](block-integrations/text.md)
|
||||
* [Todoist Comments](block-integrations/todoist/comments.md)
|
||||
* [Todoist Labels](block-integrations/todoist/labels.md)
|
||||
* [Todoist Projects](block-integrations/todoist/projects.md)
|
||||
* [Todoist Sections](block-integrations/todoist/sections.md)
|
||||
* [Todoist Tasks](block-integrations/todoist/tasks.md)
|
||||
* [Twitter Blocks](block-integrations/twitter/blocks.md)
|
||||
* [Twitter Bookmark](block-integrations/twitter/bookmark.md)
|
||||
* [Twitter Follows](block-integrations/twitter/follows.md)
|
||||
* [Twitter Hide](block-integrations/twitter/hide.md)
|
||||
* [Twitter Like](block-integrations/twitter/like.md)
|
||||
* [Twitter List Follows](block-integrations/twitter/list_follows.md)
|
||||
* [Twitter List Lookup](block-integrations/twitter/list_lookup.md)
|
||||
* [Twitter List Members](block-integrations/twitter/list_members.md)
|
||||
* [Twitter List Tweets Lookup](block-integrations/twitter/list_tweets_lookup.md)
|
||||
* [Twitter Manage](block-integrations/twitter/manage.md)
|
||||
* [Twitter Manage Lists](block-integrations/twitter/manage_lists.md)
|
||||
* [Twitter Mutes](block-integrations/twitter/mutes.md)
|
||||
* [Twitter Pinned Lists](block-integrations/twitter/pinned_lists.md)
|
||||
* [Twitter Quote](block-integrations/twitter/quote.md)
|
||||
* [Twitter Retweet](block-integrations/twitter/retweet.md)
|
||||
* [Twitter Search Spaces](block-integrations/twitter/search_spaces.md)
|
||||
* [Twitter Spaces Lookup](block-integrations/twitter/spaces_lookup.md)
|
||||
* [Twitter Timeline](block-integrations/twitter/timeline.md)
|
||||
* [Twitter Tweet Lookup](block-integrations/twitter/tweet_lookup.md)
|
||||
* [Twitter User Lookup](block-integrations/twitter/user_lookup.md)
|
||||
* [Wolfram LLM API](block-integrations/wolfram/llm_api.md)
|
||||
* [Zerobounce Validate Emails](block-integrations/zerobounce/validate_emails.md)
|
||||