Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ jobs:
run: npm install && npm run typecheck && npm test
working-directory: examples/testing/checkout-basic-testing-example

- name: Testing example (point-of-sale)
run: npm install && npm run typecheck && npm test
working-directory: examples/testing/point-of-sale-testing-example

test-build:
runs-on: ubuntu-latest

Expand Down
28 changes: 28 additions & 0 deletions examples/testing/point-of-sale-testing-example/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Environment Configuration
.env
.env.*

# Dependency directory
node_modules

# Test coverage directory
coverage

# Ignore Apple macOS Desktop Services Store
.DS_Store

# Logs
logs
*.log

# extensions build output
extensions/*/build
extensions/*/dist

# lock files



# Ignore shopify files created during app dev
.shopify/*
.shopify.lock
30 changes: 30 additions & 0 deletions examples/testing/point-of-sale-testing-example/.graphqlrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
const fs = require("node:fs");

function getConfig() {
const config = {
projects: {},
};

let extensions = [];
try {
extensions = fs.readdirSync("./extensions");
} catch {
// ignore if no extensions
}

for (const entry of extensions) {
const extensionPath = `./extensions/${entry}`;
const schema = `${extensionPath}/schema.graphql`;
if (!fs.existsSync(schema)) {
continue;
}
config.projects[entry] = {
schema,
documents: [`${extensionPath}/**/*.graphql`],
};
}

return config;
}

module.exports = getConfig();
24 changes: 24 additions & 0 deletions examples/testing/point-of-sale-testing-example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
This extension was created with:

```
shopify app init --name point-of-sale-testing-example
cd point-of-sale-testing-example
shopify app generate extension
# I chose 'POS block'
```

See it in action:

Build the root package:

```
yarn build
```

Change into this example package and run:

```
npm install
npm run typecheck
npm test
```
Empty file.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "point-of-sale-testing-example-ext",
"private": true,
"version": "1.0.0",
"license": "UNLICENSED",
"dependencies": {
"@shopify/ui-extensions": "file:../../../../packages/ui-extensions",
"preact": "^10.10.x"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import '@shopify/ui-extensions';

//@ts-ignore
declare module './src/Tile.jsx' {
const shopify: import('@shopify/ui-extensions/pos.home.tile.render').Api;
const globalThis: { shopify: typeof shopify };
}

//@ts-ignore
declare module './src/Modal.jsx' {
const shopify: import('@shopify/ui-extensions/pos.home.modal.render').Api;
const globalThis: { shopify: typeof shopify };
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Learn more about configuring your POS UI extension:
# https://shopify.dev/docs/api/pos-ui-extensions

# The version of APIs your extension will receive. Learn more:
# https://shopify.dev/docs/api/usage/versioning
api_version = "2026-04"

[[extensions]]
uid = "80f617f3-cfaf-bcfc-f4bd-8bf4faabbc38dc0e7226"
type = "ui_extension"
name = "point-of-sale-testing-example"
handle = "point-of-sale-testing-example"
description = "A preact POS UI extension"

[[extensions.targeting]]
module = "./src/Tile.jsx"
target = "pos.home.tile.render"

[[extensions.targeting]]
module = "./src/Modal.jsx"
target = "pos.home.modal.render"
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import {render} from 'preact';

export default async () => {
render(<Extension />, document.body);
};

function Extension() {
const cart = shopify.cart.current.value;
const lineItems = cart ? cart.lineItems : [];

return (
<s-page heading="Cart details">
<s-stack direction="block">
{lineItems.length === 0 ? (
<s-text>No items in cart</s-text>
) : (
lineItems.map((item) => (
<s-stack key={item.uuid} direction="inline">
<s-text>{item.title}</s-text>
<s-text>Qty: {item.quantity}</s-text>
Comment thread
kumar303 marked this conversation as resolved.
</s-stack>
))
)}
</s-stack>
</s-page>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {render} from 'preact';

export default async () => {
render(<Extension />, document.body);
};

function Extension() {
const cart = shopify.cart.current.value;
const lineItems = cart ? cart.lineItems : [];
const itemCount = lineItems.length;

function handleClick() {
shopify.action.presentModal();
}

function handleAddQuantity() {
shopify.cart.bulkCartUpdate({
lineItems: lineItems.map((item) => ({
...item,
quantity: item.quantity + 1,
})),
cartDiscounts: cart ? cart.cartDiscounts : [],
properties: cart ? cart.properties : {},
});
}

return (
<s-tile
heading="Cart summary"
subheading={`${itemCount} item(s)`}
onClick={handleClick}
>
<s-button onClick={handleAddQuantity}>Increment all quantities</s-button>
</s-tile>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/// <reference types="@shopify/ui-extensions/pos.home.modal.render" />
// ^ This defines types for custom Shopify elements supported by the target.

import {expect, test, beforeEach, afterEach} from 'vitest';
import {getExtension} from '@shopify/ui-extensions-tester';
import {createCartLineItem} from '@shopify/ui-extensions-tester/point-of-sale';

const extension = getExtension('pos.home.modal.render');

beforeEach(() => {
extension.setUp();
});

afterEach(() => {
extension.tearDown();
});

test('shows "No items in cart" when the cart is empty', async () => {
await extension.render();

const firstText = document.body.querySelector('s-text')!;
expect(firstText.textContent).toEqual('No items in cart');
});

test('shows each line item with its title and quantity', async () => {
extension.shopify.cart.current.value.lineItems = [
createCartLineItem({title: 'Widget', quantity: 3}),
createCartLineItem({title: 'Gadget', quantity: 1}),
];

await extension.render();

const textNodes = Array.from(
document.body.querySelectorAll('s-text'),
);
const textContents = textNodes.map((t) => t.textContent);
expect(textContents).toContain('Widget');
expect(textContents).toContain('Qty: 3');
expect(textContents).toContain('Gadget');
expect(textContents).toContain('Qty: 1');
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/// <reference types="@shopify/ui-extensions/pos.home.tile.render" />
// ^ This defines types for custom Shopify elements supported by the target.

import {expect, test, vi, beforeEach, afterEach} from 'vitest';
import {fireEvent, waitFor} from '@testing-library/preact';
import {getExtension} from '@shopify/ui-extensions-tester';
import {createCartLineItem} from '@shopify/ui-extensions-tester/point-of-sale';

const extension = getExtension('pos.home.tile.render');

beforeEach(() => {
extension.setUp();
});

afterEach(() => {
extension.tearDown();
});

test('shows "0 item(s)" when the cart is empty', async () => {
await extension.render();

const tile = document.body.querySelector('s-tile')!;
expect(tile.getAttribute('subheading')).toEqual('0 item(s)');
});

test('shows the line item count when the cart has items', async () => {
extension.shopify.cart.current.value.lineItems = [
createCartLineItem(),
createCartLineItem(),
];

await extension.render();

const tile = document.body.querySelector('s-tile')!;
expect(tile.getAttribute('subheading')).toEqual('2 item(s)');
});

test('calls action.presentModal when the tile is clicked', async () => {
const presentModal = vi.fn();
extension.shopify.action.presentModal = presentModal;

await extension.render();

const tile = document.body.querySelector('s-tile')!;
fireEvent.click(tile);

await waitFor(() => {
expect(presentModal).toHaveBeenCalled();
});
});

test('increments all line item quantities via bulkCartUpdate on button click', async () => {
const widget = createCartLineItem({uuid: 'widget', quantity: 2});
const gadget = createCartLineItem({uuid: 'gadget', quantity: 5});
extension.shopify.cart.current.value.lineItems = [widget, gadget];
const bulkCartUpdate = vi.spyOn(extension.shopify.cart, 'bulkCartUpdate');

await extension.render();

const button = document.body.querySelector('s-button')!;
fireEvent.click(button);

await waitFor(() => {
expect(bulkCartUpdate).toHaveBeenCalledWith(
expect.objectContaining({
lineItems: [
expect.objectContaining({uuid: 'widget', quantity: 3}),
expect.objectContaining({uuid: 'gadget', quantity: 6}),
],
}),
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "preact",
"target": "ES2020",
"checkJs": true,
"allowJs": true,
"moduleResolution": "node",
"esModuleInterop": true,
"noEmit": true,
"strict": true,
"skipLibCheck": true,
// Monorepo only: file: deps create symlinks that cause TS to
// resolve a different preact instance. Not needed with published packages.
"paths": {
"preact": ["../../node_modules/preact"],
"preact/*": ["../../node_modules/preact/*"]
}
},
"include": ["./src", "./tests", "./shopify.d.ts"]
}
Loading
Loading