Shopify模拟桥
Shopify嵌入式应用程序的全面浏览器测试解决方案。在本地模拟Shopify管理环境和App Bridge API,而不需要真正的Shopify凭据、验证码或2FA。
](https://www.npmjs.com/package/@getverdict/mock-bridge) 
🎥 在行动中看到它
▶️ 观看演示视频 -了解Mock Bridge如何简化Shopify应用程序测试
🎯 为什么使用这个包?
测试Shopify嵌入式应用程序很难:
- 🚫 Shopify管理员需要2FA和验证码
- 🤖 剧作家/自动化工具不能绕过安全
- 🔒 Chrome DevTools MCP无法与您的嵌入式Shopify应用程序交互
- 🌐 CI/CD管道需要互联网和证书
- 🐛 在真实管理中手动测试速度慢且不可靠
这个软件包解决了所有这些问题:
- ✅ 无需验证码、2FA或真实的Shopify帐户
- ✅ 全面的编剧和自动化支持
- ✅ Chrome MCP和DevTools兼容性
- ✅ 离线和CI/CD管道中工作
- ✅ 真正的数据库集成,用于全面测试
🗺️ API支持
核心功能
| 功能 | 状态 | 注释 |
|---|---|---|
| iFrame嵌入 | ✅ 支持 | 具有Polaris样式的完整管理框架 |
| 会话令牌 | ✅ 支持 | shopify.idToken() |
| 模拟环境检测 | ✅ 支持 | 自动加载模拟应用桥 |
应用桥API
| API | 状态 | 注释 |
|---|---|---|
shopify.modal | ✅ 支持 | show(), hide(), toggle() |
shopify.saveBar | ✅ 支持 | show(), hide(), toggle() |
shopify.loading | ✅ 支持 | loading(boolean) |
shopify.toast | ✅ 支持 | show(message, options) |
shopify.idToken | ✅ 支持 | 返回有效的JWT |
shopify.config | ✅ 支持 | apiKey, shop, locale |
shopify.environment | ✅ 支持 | embedded, mobile, pos |
shopify.user | ✅ 支持 | 返回模拟用户对象 |
shopify.scopes | 🔶 Stub | 返回模拟数据 |
shopify.resourcePicker | 🔶 Stub | 返回空数组 |
shopify.picker | 🔶 Stub | 返回空选择 |
shopify.scanner | 🔶 Stub | 返回模拟扫描数据 |
shopify.pos | 🔶 带有模拟数据的Stub | Cart API |
shopify.intents | 🔶 树桩 | invoke(), register() |
shopify.webVitals | 🔶 Stub | 仅限回拨注册 |
shopify.support | 🔶 Stub | 仅限回拨注册 |
shopify.reviews | 🔶 Stub | 返回成功 |
shopify.app | 🔶 Stub | 返回空扩展名 |
| 已验证获取 | ✅ 支持 | 模拟、代理或直接模式 |
| 导航 | ❌ 未执行 | |
| 打印 | ❌ 未执行 | |
| 分享 | ❌ 未执行 |
Web组件
| 组件 | 状态 | 注释 |
|---|---|---|
| `` | ✅ 支持 | 与 `` 支持 |
| `` | ✅ 支持 | 与 data-save-bar 表单集成 |
| `` | ✅ 支持 | 显示在管理员侧边栏中 |
| `` | ✅ 支持 | 内部模块 |
| `` | ❌ 未执行 |
传说: ✅ 支持|🔶 Stub(返回模拟数据)|❌ 未实现
📦 安装
npm install @getverdict/mock-bridge --save-dev
# or
yarn add @getverdict/mock-bridge --dev
# or
pnpm add @getverdict/mock-bridge --save-dev⚡ 快速启动(2个命令)
# 1. Install the package
npm install @getverdict/mock-bridge --save-dev
# 2. Start the mock (just provide your app URL)
npx @getverdict/mock-bridge http://localhost:3000就是这样! 您的应用程序现在正在模拟Shopify管理员中运行,网址为http://localhost:3080
🚀 快速入门指南
步骤1:启动模拟服务器
选项A:一个命令启动(推荐)
# Simplest - just provide your app URL
npx @getverdict/mock-bridge http://localhost:3000
# Auto-detects client ID from SHOPIFY_API_KEY environment variable
# Auto-detects common app paths and configurations选项B:配置文件
# Generate a config file
npx @getverdict/mock-bridge init
# Edit the generated mock.config.js, then run:
npx @getverdict/mock-bridge# Add to package.json scripts for easy access
{
"scripts": {
"dev": "next dev",
"mock:admin": "mock-bridge http://localhost:3000",
"dev:mock": "concurrently \"npm run dev\" \"npm run mock:admin\""
}
}选项C:程序化API
如果您需要更多的控制,您仍然可以使用编程的API:
// scripts/start-mock-admin.js
const { MockShopifyAdminServer } = require("@verdict/mock-bridge");
async function startMockAdmin() {
const server = new MockShopifyAdminServer({
appUrl: "http://localhost:3000",
clientId: process.env.SHOPIFY_API_KEY,
clientSecret: "mock-secret-12345",
port: 3080,
debug: true,
});
await server.start();
console.log("🎉 Mock Shopify Admin ready at http://localhost:3080");
}
startMockAdmin().catch(console.error);步骤2:前端集成
使您的应用程序能够检测和使用模拟环境:
选项A:自动检测(推荐)
替换您的App Bridge脚本加载:
// Check if we're in a mock environment
const urlParams = new URLSearchParams(window.location.search);
const isEmbedded = urlParams.get("embedded") === "1";
const host = urlParams.get("host");
let isMockEnvironment = false;
// Detect mock environment from URL parameters
if (isEmbedded && host) {
try {
const decodedHost = atob(host);
if (
decodedHost.includes("localhost") ||
decodedHost.includes("mock") ||
window.location.hostname === "localhost"
) {
isMockEnvironment = true;
}
} catch (e) {
// Ignore decode errors
}
}
// Load appropriate App Bridge
if (isMockEnvironment) {
console.log("Loading Mock App Bridge");
const script = document.createElement("script");
script.src = "http://localhost:3080/app-bridge.js";
script.onerror = () => {
// Fallback to real CDN if mock fails
const fallback = document.createElement("script");
fallback.src = "https://cdn.shopify.com/shopifycloud/app-bridge.js";
document.head.appendChild(fallback);
};
document.head.appendChild(script);
} else {
console.log("Loading Real Shopify App Bridge");
const script = document.createElement("script");
script.src = "https://cdn.shopify.com/shopifycloud/app-bridge.js";
document.head.appendChild(script);
}
选项B:包实用程序(TypeScript)
// app.tsx or _app.tsx
import { setupAppBridge } from "@getverdict/mock-bridge/client";
useEffect(() => {
setupAppBridge({
debug: true,
onMockDetected: (mockServerUrl) => {
console.log("Mock environment detected:", mockServerUrl);
},
onShopifyDetected: () => {
console.log("Real Shopify environment detected");
},
})
.then(() => {
console.log("App Bridge loaded successfully");
})
.catch(console.error);
}, []);步骤3:后端集成
让你的后端支持模拟会话令牌和真实会话令牌:
快速集成(替换现有的JWT验证)
// Before: Only real Shopify tokens
import { verifyShopifyJWT } from "./your-auth";
export async function authenticate(token: string) {
const authData = await verifyShopifyJWT(token);
// ... rest of auth logic
}// After: Support both real and mock tokens
import {
validateSessionToken,
createMockUser,
} from "@getverdict/mock-bridge/auth";
export async function authenticate(token: string) {
const authData = await validateSessionToken(token, {
shopifySecret: process.env.SHOPIFY_API_PRIVATE_KEY!,
});
if (!authData) {
throw new Error("Invalid token");
}
// Get shop from your database (same for both mock and real)
const shop = await getShopByName(authData.shopName);
if (!shop) {
throw new Error("Shop not found");
}
if (authData.isMock) {
// Mock environment - skip Shopify API calls
return {
shop,
currentUser: createMockUser({
shopName: authData.shopName,
permissions: shop.settings?.defaultStaffPermissions,
}),
isMock: true,
};
} else {
// Real environment - proceed with normal Shopify flow
const currentUser = await exchangeTokenForUser(token, shop);
return {
shop,
currentUser,
isMock: false,
};
}
}框架特定示例
Next.js API路线:
// pages/api/products.ts
import { validateSessionToken } from "@getverdict/mock-bridge/auth";
export default async function handler(req, res) {
const token = req.headers.authorization?.replace("Bearer ", "");
const authData = await validateSessionToken(token, {
shopifySecret: process.env.SHOPIFY_API_PRIVATE_KEY!,
});
if (!authData) {
return res.status(401).json({ error: "Unauthorized" });
}
if (authData.isMock) {
// Return mock data for testing
return res.json({
products: [
{ id: "1", title: "Mock Product 1", price: "19.99" },
{ id: "2", title: "Mock Product 2", price: "29.99" },
],
});
} else {
// Fetch real products from Shopify
const products = await fetchShopifyProducts(authData.shopName);
return res.json({ products });
}
}Express.js中间件:
import {
validateSessionToken,
createMockUser,
} from "@getverdict/mock-bridge/auth";
function createAuthMiddleware() {
return async (req, res, next) => {
const token = req.headers.authorization?.replace("Bearer ", "");
const authData = await validateSessionToken(token, {
shopifySecret: process.env.SHOPIFY_API_PRIVATE_KEY!,
});
if (!authData) {
return res.status(401).json({ error: "Unauthorized" });
}
const shop = await getShopByName(authData.shopName);
req.shop = shop;
req.isMockAuth = authData.isMock;
if (authData.isMock) {
req.currentUser = createMockUser({ shopName: authData.shopName });
} else {
req.currentUser = await exchangeTokenForUser(token, shop);
}
next();
};
}
app.use("/api/*", createAuthMiddleware());步骤4:数据库设置
确保您的模拟商店存在于数据库中:
// Add this to your database seed or setup script
async function setupMockShop() {
const mockShop = {
name: "test-shop.myshopify.com",
displayName: "Mock Test Shop",
accessToken: "mock-access-token",
active: true,
settings: {
defaultStaffPermissions: [
"read_products",
"write_products",
"read_orders",
"write_orders",
],
},
};
await createOrUpdateShop(mockShop);
console.log("Mock shop created for testing");
}
// Run during development setup
if (process.env.NODE_ENV === "development") {
setupMockShop();
}步骤5:开始开发
# Option 1: CLI command (simplest)
npx @getverdict/mock-bridge http://localhost:3000
# Option 2: Package.json scripts
npm run dev:mock
# Option 3: Separate terminals
npm run dev # Terminal 1: Your app
npm run mock:admin # Terminal 2: Mock admin然后导航到:
- 您的应用程序: http://localhost:3000
- 模拟Shopify管理员: http://localhost:3080
- 你的应用程序嵌入在mock-admin中: http://localhost:3080(自动嵌入您的应用程序)
🖥️ CLI参考
快速命令
# Basic usage with auto-detection
npx @getverdict/mock-bridge http://localhost:3000
# If installed locally, you can use the shorter command:
# npm install @getverdict/mock-bridge --save-dev
# npx mock-bridge http://localhost:3000
# Full configuration
npx @getverdict/mock-bridge http://localhost:3000/shopify \
--client-id your-client-id \
--port 3080 \
--debug
# Using config file
npx @getverdict/mock-bridge init # Create config file
npx @getverdict/mock-bridge # Use config file
# Help and version
npx @getverdict/mock-bridge --help
npx @getverdict/mock-bridge --versionCLI选项
| 选项 | 描述 | 默认值 |
|---|---|---|
[app-url] | 您的应用程序的URL(位置参数) | 从package.json中自动检测到 |
--client-id | Shopify应用程序客户端ID | $SHOPIFY_API_KEY |
--client-secret | 模拟客户端机密 | "mock-secret-12345" |
--shop | 模拟商店域名 | "test-shop.myshopify.com" |
--port | 模拟管理端口 | 3080 |
--config | 配置文件路径 | "mock.config.js" |
--debug | 启用调试日志记录 | false |
环境变量
CLI会自动读取这些环境变量:
SHOPIFY_API_KEY=your-client-id # Used for --client-id
NODE_ENV=development # Enables mock token support配置文件
生成配置文件 npx @getverdict/mock-bridge init:
// mock.config.js
module.exports = {
appUrl: "http://localhost:3000/shopify", // Include path in URL
clientId: process.env.SHOPIFY_API_KEY,
clientSecret: "mock-secret-12345",
port: 3080,
shop: "test-shop.myshopify.com",
debug: true,
scopes: ["read_products", "write_products", "read_orders", "write_orders"],
};🎭 运作原理
架构概述
┌─────────────────────────────────────┐
│ Mock Shopify Admin (Port 3080) │
│ ┌─────────────────────────────┐ │
│ │ Shopify Admin UI │ │
│ │ (Navigation, etc.) │ │
│ └─────────────────────────────┘ │
│ ┌─────────────────────────────┐ │
│ │ Your App (iframe) │ │ ← Embedded like real Shopify
│ │ - Mock App Bridge loaded │ │
│ │ - Gets mock session tokens │ │
│ │ - Makes API calls to your │ │
│ │ backend │ │
│ └─────────────────────────────┘ │
└─────────────────────────────────────┘
↕ PostMessage API
┌─────────────────────────────────────┐
│ Your Backend │
│ - Validates mock tokens │ ← Same backend, enhanced auth
│ - Skips Shopify API calls │
│ - Uses real database │
│ - Returns mock/real data │
└─────────────────────────────────────┘模拟与真实流程
模拟环境(测试):
- Mock-admin在iframe中为您的应用程序提供服务
- 模拟应用桥提供会话令牌
- 您的前端对后端进行API调用
- 后端检测模拟令牌并跳过Shopify API
- 返回模拟数据或直接使用数据库
真实环境(生产):
- 真正的Shopify管理员在iframe中为您的应用程序提供服务
- Real App Bridge提供会话令牌
- 您的前端对后端进行API调用
- 后端检测真实令牌并调用Shopify API
- 从Shopify返回真实数据
🔧 配置选项
模拟服务器配置
const server = new MockShopifyAdminServer({
// Required
appUrl: "http://localhost:3000/shopify", // Your app's URL (with path)
clientId: "your-shopify-client-id", // Your Shopify app's client ID
clientSecret: "mock-secret-12345", // Mock secret (dev only)
// Optional
port: 3080, // Mock admin port
shop: "test-shop.myshopify.com", // Mock shop domain
apiVersion: "2024-01", // Shopify API version
scopes: [
// Your app's scopes
"read_products",
"write_products",
"read_orders",
"write_orders",
],
debug: true, // Enable debug logging
// Admin API handling (see below)
adminApi: "mock",
});管理员API配置
控制方式 fetch('/admin/api/...') 处理请求:
// mock.config.js
// Option 1: Mock data (default) - returns fake data, works offline
module.exports = {
adminApi: "mock",
};
// Option 2: Proxy through your app - for real data via your backend
module.exports = {
adminApi: {
proxy: "http://localhost:3000/api/shopify-proxy",
},
};
// Option 3: Direct to Shopify - requires access token from installed shop
module.exports = {
adminApi: {
accessToken: process.env.SHOPIFY_ACCESS_TOKEN,
},
};何时使用每种模式:
| 模式 | 用例 |
|---|---|
'mock' | 离线测试、CI/CD、无需Shopify凭据 |
{ proxy: '...' } | 通过应用程序的后端代理进行真实数据测试 |
{ accessToken: '...' } | 直接访问Shopify API(需要安装商店) |
代理端点示例 (如果使用代理模式):
// pages/api/shopify-proxy.ts (Next.js example)
export default async function handler(req, res) {
const { url, method, body } = req.body;
const shop = await getShopFromSession(req);
const response = await fetch(`https://${shop.domain}${url}`, {
method,
headers: {
"X-Shopify-Access-Token": shop.accessToken,
"Content-Type": "application/json",
},
body: body ? JSON.stringify(body) : undefined,
});
res.json(await response.json());
}身份验证选项
const authData = await validateSessionToken(token, {
shopifySecret: process.env.SHOPIFY_API_PRIVATE_KEY!, // Required
mockSecret: "custom-mock-secret", // Optional
developmentOnly: true, // Only try mock in dev
});模拟用户选项
const mockUser = createMockUser({
shopName: "test-shop.myshopify.com",
userId: "123456789", // Custom user ID
email: "test@shop.com", // Custom email
firstName: "Test", // Custom first name
lastName: "User", // Custom last name
permissions: ["read_products"], // Custom permissions
additionalProps: {
// Any additional props
posBillingTermsAcceptedAt: new Date(),
customField: "value",
},
});🎪 使用自动化工具进行测试
剧作家示例
// playwright.config.ts
import { defineConfig } from "@playwright/test";
import { MockShopifyAdminServer } from "@getverdict/mock-bridge";
let mockServer: MockShopifyAdminServer;
export default defineConfig({
globalSetup: async () => {
mockServer = new MockShopifyAdminServer({
appUrl: "http://localhost:3000",
clientId: "test-client-id",
clientSecret: "mock-secret-12345",
port: 3080,
});
await mockServer.start();
},
globalTeardown: async () => {
await mockServer?.stop();
},
use: {
baseURL: "http://localhost:3080",
},
});// tests/app.spec.ts
import { test, expect } from "@playwright/test";
test("should load app in mock Shopify admin", async ({ page }) => {
await page.goto("/");
// Wait for app to load in iframe
const appFrame = page.frameLocator("#app-iframe");
// Test your app functionality
await expect(appFrame.locator("h1")).toContainText("Your App Title");
// Test App Bridge actions
await appFrame.locator('button:has-text("Show Toast")').click();
await expect(page.locator(".toast")).toContainText("Success!");
// Test API calls
await appFrame.locator('button:has-text("Load Products")').click();
await expect(appFrame.locator(".product-list")).toBeVisible();
});
test("should handle authentication", async ({ page }) => {
await page.goto("/");
const appFrame = page.frameLocator("#app-iframe");
// Your app should be authenticated automatically
await expect(appFrame.locator(".user-info")).toContainText("Mock User");
await expect(appFrame.locator(".shop-info")).toContainText(
"test-shop.myshopify.com"
);
});Jest集成测试
// tests/api.test.ts
import { MockShopifyAdminServer } from "@getverdict/mock-bridge";
import {
createMockUser,
validateSessionToken,
} from "@verdict/shopify-app-bridge-mock/auth";
describe("API with Mock Tokens", () => {
let mockServer: MockShopifyAdminServer;
beforeAll(async () => {
mockServer = new MockShopifyAdminServer({
appUrl: "http://localhost:3000",
clientId: "test-client",
clientSecret: "mock-secret-12345",
});
await mockServer.start();
});
afterAll(async () => {
await mockServer.stop();
});
it("should authenticate with mock token", async () => {
// Generate mock token
const token = mockServer.tokenGenerator.generateSessionToken({
shop: "test-shop.myshopify.com",
clientId: "test-client",
clientSecret: "mock-secret-12345",
});
// Test authentication
const authData = await validateSessionToken(token, {
shopifySecret: "real-secret",
mockSecret: "mock-secret-12345",
});
expect(authData?.isMock).toBe(true);
expect(authData?.shopName).toBe("test-shop.myshopify.com");
});
it("should create mock users", async () => {
const mockUser = createMockUser({
shopName: "test-shop.myshopify.com",
permissions: ["read_products"],
});
expect(mockUser.email).toBe("mock@test-shop.com");
expect(mockUser.permissions).toContain("read_products");
});
});🛡️ 安全注意事项
仅限开发
模拟令牌仅用于开发和测试:
// ✅ Good: Mock tokens only in development
const authData = await validateSessionToken(token, {
shopifySecret: process.env.SHOPIFY_API_PRIVATE_KEY!,
developmentOnly: true, // Default: true
});
// ❌ Bad: Never allow mock tokens in production
const authData = await validateSessionToken(token, {
shopifySecret: process.env.SHOPIFY_API_PRIVATE_KEY!,
developmentOnly: false, // Don't do this!
});环境变量
将模拟秘密排除在生产之外:
# .env.local (development only)
SHOPIFY_API_PRIVATE_KEY=your-real-shopify-secret
MOCK_SECRET=mock-secret-12345
# .env.production (no mock secrets)
SHOPIFY_API_PRIVATE_KEY=your-real-shopify-secret
# No MOCK_SECRET in production!数据库要求
模拟代币仍然需要数据库中的有效商店:
// Always verify shop exists, even for mock tokens
const authData = await validateSessionToken(token, options);
if (authData) {
const shop = await getShopByName(authData.shopName);
if (!shop) {
// Reject both mock and real tokens if shop doesn't exist
throw new Error("Shop not found");
}
}🐛 故障排除
常见问题
应用程序未在mock-admin中加载:
- 检查CSP标头是否允许嵌入iframe
- 在mock配置中验证您的应用程序URL是否正确
- 确保您的应用程序接受
embedded=1参数
模拟令牌不起作用:
- 验证
NODE_ENV=development - 检查前端和后端之间的模拟秘密匹配
- 确保商店存在于您的数据库中
真实代币损坏:
- 验证
SHOPIFY_API_PRIVATE_KEY设置正确 - 检查是否将真正的秘密传递给
validateSessionToken - 确保保留现有的Shopify身份验证流
调试模式
启用调试日志以排除故障:
// Mock server with debug
const server = new MockShopifyAdminServer({
// ... config
debug: true,
});
// Frontend with debug
setupAppBridge({
debug: true,
onMockDetected: (url) => console.log("Mock detected:", url),
onShopifyDetected: () => console.log("Real Shopify detected"),
});环境检查
验证您的环境设置:
// Add to your app startup
console.log("Environment:", process.env.NODE_ENV);
console.log("Has Shopify Secret:", !!process.env.SHOPIFY_API_PRIVATE_KEY);
console.log("Mock tokens enabled:", process.env.NODE_ENV === "development");📚 API 参考
身份验证功能
validateSessionToken(token, options)
真实和模拟代币的通用验证器。
参数:
token: string-JWT会话令牌options.shopifySecret: string-真正的Shopify客户机密options.mockSecret?: string-模拟秘密(默认值:“Mock-secret-12345”)options.developmentOnly?: boolean-在开发中只尝试模拟令牌(默认值:true)
退货: AuthResult | false
isMockToken(token, mockSecret?)
快速检查令牌是否为模拟令牌。
退货: boolean
createMockUser(options?)
生成用于测试的模拟用户对象。
退货: MockCurrentUser
withMockTokenSupport(authFunction, shopifySecret, options?)
包装器,为现有的身份验证函数添加模拟支持。
服务器类
MockShopifyAdminServer
主模拟服务器类。
方法:
start()-启动模拟服务器stop()-停止模拟服务器getConfig()-获取服务器配置
客户端实用程序
setupAppBridge(options?)
自动检测模拟环境并加载适当的App Bridge。
退货: Promise
有关API的完整文档,请参阅 后端集成指南.
🤝 贡献
欢迎投稿!请随时提交问题、功能请求或拉取请求。
开发环境
git clone
cd packages/shopify-app-bridge-mock
pnpm install
pnpm build
pnpm test📄 许可证
MIT许可证-有关详细信息,请参阅许可证文件。
🙏 致谢
- 专为Shopify开发者社区打造
- 受到对更好测试工具需求的启发
- 感谢所有贡献者和用户
______________________________________________________________________
制作❤️ 适用于希望无痛测试的Shopify开发人员
