├── CODEOWNERS ├── import-data ├── channels.json ├── channel-types.json ├── customer-types.json ├── order-types.json ├── customer-groups.json ├── orders.csv ├── project.json ├── stores.json ├── zones.json ├── product-type.json ├── products-ci.csv ├── tax-category.json ├── customers.json ├── categories.csv ├── shipping-methods.json └── products.csv ├── third_party └── storefront │ ├── lang │ └── .gitkeep │ ├── .npmrc │ ├── pages │ ├── README.md │ ├── MyAccount │ │ ├── MyReviews.vue │ │ ├── LoyaltyCard.vue │ │ ├── MyNewsletter.vue │ │ ├── MyProfile.vue │ │ ├── BillingDetails.vue │ │ └── ShippingDetails.vue │ ├── Checkout.vue │ ├── MyAccount.vue │ └── ResetPassword.vue │ ├── components │ ├── README.md │ ├── UserBillingAddress.vue │ ├── UserShippingAddress.vue │ ├── RelatedProducts.vue │ ├── Checkout │ │ ├── VsfPaymentProvider.vue │ │ ├── UserShippingAddresses.vue │ │ ├── UserBillingAddresses.vue │ │ └── VsfPaymentProviderMock.vue │ ├── MobileStoreBanner.vue │ ├── BottomNavigation.vue │ ├── HeaderNavigation.vue │ ├── Notification.vue │ ├── LocaleSelector.vue │ ├── AppFooter.vue │ ├── InstagramFeed.vue │ ├── MyAccount │ │ ├── PasswordResetForm.vue │ │ └── ProfileUpdateForm.vue │ └── StoreLocaleSelector.vue │ ├── helpers │ ├── README.md │ ├── category │ │ ├── getCategoryPath.js │ │ ├── index.js │ │ └── getCategorySearchParameters.js │ ├── validators │ │ └── phone.ts │ ├── cacheControl.js │ └── Checkout │ │ └── getShippingMethodPrice.ts │ ├── static │ ├── favicon.ico │ ├── homepage │ │ ├── apple.png │ │ ├── bannerD.png │ │ ├── google.png │ │ ├── bannerA.webp │ │ ├── bannerB.webp │ │ ├── bannerC.webp │ │ ├── bannerE.webp │ │ ├── bannerF.webp │ │ ├── bannerG.webp │ │ ├── bannerH.webp │ │ ├── imageAd.webp │ │ ├── imageAm.webp │ │ ├── imageBd.webp │ │ ├── imageBm.webp │ │ ├── imageCd.webp │ │ ├── imageCm.webp │ │ ├── imageDd.webp │ │ ├── imageDm.webp │ │ ├── productA.webp │ │ ├── productB.webp │ │ ├── productC.webp │ │ └── newsletter.webp │ ├── icons │ │ ├── langs │ │ │ ├── de.webp │ │ │ └── en.webp │ │ └── facebook.svg │ ├── img │ │ └── products │ │ │ ├── mug.jpg │ │ │ ├── watch.jpg │ │ │ ├── loafers.jpg │ │ │ ├── tank-top.jpg │ │ │ ├── hairdryer.jpg │ │ │ ├── sunglasses.jpg │ │ │ ├── candle-holder.jpg │ │ │ ├── bamboo-glass-jar.jpg │ │ │ └── salt-and-pepper-shakers.jpg │ ├── images │ │ ├── VRHeadsets.png │ │ ├── HeroBannerImage2.png │ │ ├── Advert2BannerImage.png │ │ ├── AdvertBannerImage.png │ │ ├── credits.txt │ │ ├── folded-clothes-on-white-chair.jpg │ │ └── folded-clothes-on-white-chair-wide.jpg │ ├── thank-you │ │ ├── bannerD.png │ │ └── bannerM.png │ ├── css │ │ └── main.scss │ └── README.md │ ├── middleware │ ├── is-authenticated.js │ ├── README.md │ └── checkout.js │ ├── tests │ └── e2e │ │ ├── types │ │ ├── address.ts │ │ ├── customer.ts │ │ └── types.ts │ │ ├── support │ │ ├── index.d.ts │ │ ├── commands.js │ │ └── index.js │ │ ├── pages │ │ ├── my-account.ts │ │ ├── home.ts │ │ ├── base.ts │ │ ├── utils │ │ │ └── element.ts │ │ ├── product.ts │ │ ├── components │ │ │ ├── header.ts │ │ │ ├── login-modal.ts │ │ │ └── cart-sidebar.ts │ │ ├── factory.ts │ │ ├── category.ts │ │ └── checkout.ts │ │ ├── fixtures │ │ └── test-data │ │ │ ├── e2e-user-login.json │ │ │ ├── e2e-user-registration.json │ │ │ ├── e2e-add-to-cart.json │ │ │ ├── e2e-remove-from-cart.json │ │ │ ├── e2e-product-page.json │ │ │ ├── e2e-update-cart.json │ │ │ ├── e2e-place-order.json │ │ │ ├── e2e-checkout-order-summary.json │ │ │ └── e2e-carts-merging.json │ │ ├── utils │ │ ├── data-generator.ts │ │ └── network.ts │ │ ├── cypress.json │ │ ├── plugins │ │ └── index.js │ │ ├── integration │ │ ├── e2e-add-to-cart.spec.ts │ │ ├── e2e-user-login.spec.ts │ │ ├── e2e-user-registration.spec.ts │ │ ├── e2e-remove-from-cart.spec.ts │ │ ├── e2e-product-page.spec.ts │ │ ├── e2e-update-cart.spec.ts │ │ ├── e2e-checkout-order-summary.spec.ts │ │ ├── e2e-place-order.spec.ts │ │ ├── e2e-carts-merging.spec.ts │ │ ├── e2e-checkout-shipping-validation.spec.ts │ │ └── e2e-checkout-billing-validation.spec.ts │ │ └── api │ │ └── requests.ts │ ├── composables │ ├── index.ts │ ├── usePaymentProviderMock │ │ └── index.ts │ ├── useUiNotification │ │ └── index.ts │ ├── useUiState.ts │ └── useUiHelpers │ │ └── index.ts │ ├── .editorconfig │ ├── .babelrc │ ├── shims-webpack.d.ts │ ├── jest.config.js │ ├── themeConfig.js │ ├── Dockerfile │ ├── tsconfig.json │ ├── middleware.config.js │ ├── README.md │ ├── layouts │ ├── account.vue │ ├── blank.vue │ ├── error.vue │ └── default.vue │ ├── package.json │ ├── .dockerignore │ ├── .gcloudignore │ ├── .gitignore │ └── nuxt.config.js ├── .gitignore ├── assets └── images │ ├── storefront.png │ └── commercetools-freetrial.png └── CONTRIBUTING.md /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @verbanicm 2 | -------------------------------------------------------------------------------- /import-data/channels.json: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /import-data/channel-types.json: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /import-data/customer-types.json: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /import-data/order-types.json: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /third_party/storefront/lang/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /import-data/customer-groups.json: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /third_party/storefront/.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /third_party/storefront/pages/README.md: -------------------------------------------------------------------------------- 1 | Put here theme-specific pages to override default ones -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore child repo 2 | commercetools-sunrise-data 3 | 4 | # Ignore env file 5 | set_env.sh -------------------------------------------------------------------------------- /third_party/storefront/components/README.md: -------------------------------------------------------------------------------- 1 | Put here theme-specific components to override default ones -------------------------------------------------------------------------------- /import-data/orders.csv: -------------------------------------------------------------------------------- 1 | customerEmail,orderNumber,lineitems.variant.sku,lineitems.price,lineitems.quantity,totalPrice -------------------------------------------------------------------------------- /import-data/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "currencies": ["USD"], 3 | "countries": ["US"], 4 | "languages": ["EN"] 5 | } 6 | -------------------------------------------------------------------------------- /third_party/storefront/helpers/README.md: -------------------------------------------------------------------------------- 1 | Put here platform-specific, non-agnostic functions that overwrite default code. 2 | -------------------------------------------------------------------------------- /assets/images/storefront.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/headless-commerce-demo/HEAD/assets/images/storefront.png -------------------------------------------------------------------------------- /import-data/stores.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "key": "default", 4 | "name": { 5 | "en": "Default" 6 | } 7 | } 8 | ] 9 | -------------------------------------------------------------------------------- /import-data/zones.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "United States", 4 | "description": "", 5 | "locations": [{ "country": "US" }] 6 | } 7 | ] 8 | -------------------------------------------------------------------------------- /assets/images/commercetools-freetrial.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/headless-commerce-demo/HEAD/assets/images/commercetools-freetrial.png -------------------------------------------------------------------------------- /import-data/product-type.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "main", 4 | "key": "main", 5 | "description": "", 6 | "attributes": [] 7 | } 8 | ] 9 | -------------------------------------------------------------------------------- /third_party/storefront/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/headless-commerce-demo/HEAD/third_party/storefront/static/favicon.ico -------------------------------------------------------------------------------- /third_party/storefront/static/homepage/apple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/headless-commerce-demo/HEAD/third_party/storefront/static/homepage/apple.png -------------------------------------------------------------------------------- /third_party/storefront/static/homepage/bannerD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/headless-commerce-demo/HEAD/third_party/storefront/static/homepage/bannerD.png -------------------------------------------------------------------------------- /third_party/storefront/static/homepage/google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/headless-commerce-demo/HEAD/third_party/storefront/static/homepage/google.png -------------------------------------------------------------------------------- /third_party/storefront/static/icons/langs/de.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/headless-commerce-demo/HEAD/third_party/storefront/static/icons/langs/de.webp -------------------------------------------------------------------------------- /third_party/storefront/static/icons/langs/en.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/headless-commerce-demo/HEAD/third_party/storefront/static/icons/langs/en.webp -------------------------------------------------------------------------------- /third_party/storefront/static/img/products/mug.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/headless-commerce-demo/HEAD/third_party/storefront/static/img/products/mug.jpg -------------------------------------------------------------------------------- /third_party/storefront/static/homepage/bannerA.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/headless-commerce-demo/HEAD/third_party/storefront/static/homepage/bannerA.webp -------------------------------------------------------------------------------- /third_party/storefront/static/homepage/bannerB.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/headless-commerce-demo/HEAD/third_party/storefront/static/homepage/bannerB.webp -------------------------------------------------------------------------------- /third_party/storefront/static/homepage/bannerC.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/headless-commerce-demo/HEAD/third_party/storefront/static/homepage/bannerC.webp -------------------------------------------------------------------------------- /third_party/storefront/static/homepage/bannerE.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/headless-commerce-demo/HEAD/third_party/storefront/static/homepage/bannerE.webp -------------------------------------------------------------------------------- /third_party/storefront/static/homepage/bannerF.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/headless-commerce-demo/HEAD/third_party/storefront/static/homepage/bannerF.webp -------------------------------------------------------------------------------- /third_party/storefront/static/homepage/bannerG.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/headless-commerce-demo/HEAD/third_party/storefront/static/homepage/bannerG.webp -------------------------------------------------------------------------------- /third_party/storefront/static/homepage/bannerH.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/headless-commerce-demo/HEAD/third_party/storefront/static/homepage/bannerH.webp -------------------------------------------------------------------------------- /third_party/storefront/static/homepage/imageAd.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/headless-commerce-demo/HEAD/third_party/storefront/static/homepage/imageAd.webp -------------------------------------------------------------------------------- /third_party/storefront/static/homepage/imageAm.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/headless-commerce-demo/HEAD/third_party/storefront/static/homepage/imageAm.webp -------------------------------------------------------------------------------- /third_party/storefront/static/homepage/imageBd.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/headless-commerce-demo/HEAD/third_party/storefront/static/homepage/imageBd.webp -------------------------------------------------------------------------------- /third_party/storefront/static/homepage/imageBm.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/headless-commerce-demo/HEAD/third_party/storefront/static/homepage/imageBm.webp -------------------------------------------------------------------------------- /third_party/storefront/static/homepage/imageCd.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/headless-commerce-demo/HEAD/third_party/storefront/static/homepage/imageCd.webp -------------------------------------------------------------------------------- /third_party/storefront/static/homepage/imageCm.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/headless-commerce-demo/HEAD/third_party/storefront/static/homepage/imageCm.webp -------------------------------------------------------------------------------- /third_party/storefront/static/homepage/imageDd.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/headless-commerce-demo/HEAD/third_party/storefront/static/homepage/imageDd.webp -------------------------------------------------------------------------------- /third_party/storefront/static/homepage/imageDm.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/headless-commerce-demo/HEAD/third_party/storefront/static/homepage/imageDm.webp -------------------------------------------------------------------------------- /third_party/storefront/static/homepage/productA.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/headless-commerce-demo/HEAD/third_party/storefront/static/homepage/productA.webp -------------------------------------------------------------------------------- /third_party/storefront/static/homepage/productB.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/headless-commerce-demo/HEAD/third_party/storefront/static/homepage/productB.webp -------------------------------------------------------------------------------- /third_party/storefront/static/homepage/productC.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/headless-commerce-demo/HEAD/third_party/storefront/static/homepage/productC.webp -------------------------------------------------------------------------------- /third_party/storefront/static/images/VRHeadsets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/headless-commerce-demo/HEAD/third_party/storefront/static/images/VRHeadsets.png -------------------------------------------------------------------------------- /third_party/storefront/static/img/products/watch.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/headless-commerce-demo/HEAD/third_party/storefront/static/img/products/watch.jpg -------------------------------------------------------------------------------- /third_party/storefront/static/thank-you/bannerD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/headless-commerce-demo/HEAD/third_party/storefront/static/thank-you/bannerD.png -------------------------------------------------------------------------------- /third_party/storefront/static/thank-you/bannerM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/headless-commerce-demo/HEAD/third_party/storefront/static/thank-you/bannerM.png -------------------------------------------------------------------------------- /third_party/storefront/static/homepage/newsletter.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/headless-commerce-demo/HEAD/third_party/storefront/static/homepage/newsletter.webp -------------------------------------------------------------------------------- /third_party/storefront/static/img/products/loafers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/headless-commerce-demo/HEAD/third_party/storefront/static/img/products/loafers.jpg -------------------------------------------------------------------------------- /third_party/storefront/static/img/products/tank-top.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/headless-commerce-demo/HEAD/third_party/storefront/static/img/products/tank-top.jpg -------------------------------------------------------------------------------- /third_party/storefront/static/images/HeroBannerImage2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/headless-commerce-demo/HEAD/third_party/storefront/static/images/HeroBannerImage2.png -------------------------------------------------------------------------------- /third_party/storefront/static/img/products/hairdryer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/headless-commerce-demo/HEAD/third_party/storefront/static/img/products/hairdryer.jpg -------------------------------------------------------------------------------- /third_party/storefront/static/img/products/sunglasses.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/headless-commerce-demo/HEAD/third_party/storefront/static/img/products/sunglasses.jpg -------------------------------------------------------------------------------- /third_party/storefront/static/css/main.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --c-primary: #4bc7c7 !important; 3 | } 4 | 5 | .app-logo img { 6 | width: 120px !important; 7 | height: auto !important; 8 | } 9 | -------------------------------------------------------------------------------- /third_party/storefront/static/images/Advert2BannerImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/headless-commerce-demo/HEAD/third_party/storefront/static/images/Advert2BannerImage.png -------------------------------------------------------------------------------- /third_party/storefront/static/images/AdvertBannerImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/headless-commerce-demo/HEAD/third_party/storefront/static/images/AdvertBannerImage.png -------------------------------------------------------------------------------- /third_party/storefront/static/img/products/candle-holder.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/headless-commerce-demo/HEAD/third_party/storefront/static/img/products/candle-holder.jpg -------------------------------------------------------------------------------- /third_party/storefront/helpers/category/getCategoryPath.js: -------------------------------------------------------------------------------- 1 | export function getCategoryPath(category, context = this) { 2 | return `/c/${context.$route.params.slug_1}/${category.slug}`; 3 | } 4 | -------------------------------------------------------------------------------- /third_party/storefront/static/img/products/bamboo-glass-jar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/headless-commerce-demo/HEAD/third_party/storefront/static/img/products/bamboo-glass-jar.jpg -------------------------------------------------------------------------------- /third_party/storefront/static/images/credits.txt: -------------------------------------------------------------------------------- 1 | folded-clothes-on-white-chair.jpg,,https://unsplash.com/photos/fr0J5-GIVyg 2 | folded-clothes-on-white-chair-wide.jpg,,https://unsplash.com/photos/fr0J5-GIVyg 3 | -------------------------------------------------------------------------------- /third_party/storefront/static/images/folded-clothes-on-white-chair.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/headless-commerce-demo/HEAD/third_party/storefront/static/images/folded-clothes-on-white-chair.jpg -------------------------------------------------------------------------------- /third_party/storefront/static/img/products/salt-and-pepper-shakers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/headless-commerce-demo/HEAD/third_party/storefront/static/img/products/salt-and-pepper-shakers.jpg -------------------------------------------------------------------------------- /third_party/storefront/static/images/folded-clothes-on-white-chair-wide.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlecodelabs/headless-commerce-demo/HEAD/third_party/storefront/static/images/folded-clothes-on-white-chair-wide.jpg -------------------------------------------------------------------------------- /third_party/storefront/middleware/is-authenticated.js: -------------------------------------------------------------------------------- 1 | export default async ({ app, redirect }) => { 2 | if (!app.$cookies.get('vsf-commercetools-token')?.scope?.includes('customer_id')) { 3 | return redirect('/'); 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /third_party/storefront/tests/e2e/types/address.ts: -------------------------------------------------------------------------------- 1 | export type Address = { 2 | streetName: string; 3 | apartment: string; 4 | city: string; 5 | state: string; 6 | country: string; 7 | zipcode: string; 8 | phone: string; 9 | } 10 | -------------------------------------------------------------------------------- /third_party/storefront/composables/index.ts: -------------------------------------------------------------------------------- 1 | import useUiHelpers from './useUiHelpers'; 2 | import useUiState from './useUiState'; 3 | import useUiNotification from './useUiNotification'; 4 | 5 | export { 6 | useUiHelpers, 7 | useUiState, 8 | useUiNotification 9 | }; 10 | -------------------------------------------------------------------------------- /third_party/storefront/tests/e2e/support/index.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable spaced-comment */ 2 | /// 3 | /// 4 | 5 | declare namespace Cypress { 6 | interface Chainable { 7 | fixtures?: any; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /third_party/storefront/tests/e2e/pages/my-account.ts: -------------------------------------------------------------------------------- 1 | import { contains } from './utils/element'; 2 | 3 | class Sidebar { 4 | get heading(): Cypress.Chainable { 5 | return contains('my-account-content-pages', 'My Account'); 6 | } 7 | } 8 | 9 | export { 10 | Sidebar 11 | }; 12 | -------------------------------------------------------------------------------- /third_party/storefront/composables/usePaymentProviderMock/index.ts: -------------------------------------------------------------------------------- 1 | import { sharedRef } from '@vue-storefront/core'; 2 | 3 | export const usePaymentProviderMock = () => { 4 | const status = sharedRef(false, 'usePaymentProviderMock-status'); 5 | 6 | return { 7 | status 8 | }; 9 | }; 10 | -------------------------------------------------------------------------------- /third_party/storefront/.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /third_party/storefront/helpers/category/index.js: -------------------------------------------------------------------------------- 1 | import { getCategorySearchParameters } from './getCategorySearchParameters'; 2 | import { getCategoryPath } from './getCategoryPath'; 3 | 4 | // TODO: remove, use faceting instead 5 | export { 6 | getCategorySearchParameters, 7 | getCategoryPath 8 | }; 9 | -------------------------------------------------------------------------------- /import-data/products-ci.csv: -------------------------------------------------------------------------------- 1 | productType,variantId,sku,prices,tax,categories,images,name.en,description.en,slug.en,metaTitle.en,metaDescription.en,metaKeywords.en,name.de,description.de,slug.de,metaTitle.de,metaDescription.de,metaKeywords.de,name.it,description.it,slug.it,metaTitle.it,metaDescription.it,metaKeywords.it -------------------------------------------------------------------------------- /third_party/storefront/tests/e2e/types/customer.ts: -------------------------------------------------------------------------------- 1 | import { Address } from './address'; 2 | 3 | export type Customer = { 4 | firstName?: string; 5 | lastName?: string; 6 | email?: string; 7 | password?: string; 8 | address?: { 9 | shipping: Address, 10 | billing: Address 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /third_party/storefront/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "test": { 4 | "presets": [ 5 | [ 6 | "@babel/preset-env", 7 | { 8 | "targets": { 9 | "node": "current" 10 | } 11 | } 12 | ] 13 | ] 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /third_party/storefront/helpers/category/getCategorySearchParameters.js: -------------------------------------------------------------------------------- 1 | export const getCategorySearchParameters = (context) => { 2 | const { params } = context.root.$route; 3 | const lastSlug = Object.keys(params).reduce((prev, curr) => params[curr] || prev, params.slug_1); 4 | 5 | return { 6 | slug: lastSlug 7 | }; 8 | }; 9 | -------------------------------------------------------------------------------- /third_party/storefront/tests/e2e/pages/home.ts: -------------------------------------------------------------------------------- 1 | import Base from './base'; 2 | import Header from './components/header'; 3 | 4 | class Home extends Base { 5 | get header() { 6 | return Header; 7 | } 8 | 9 | visit(): Cypress.Chainable { 10 | return cy.visit('/'); 11 | } 12 | } 13 | 14 | export default new Home(); 15 | -------------------------------------------------------------------------------- /third_party/storefront/helpers/validators/phone.ts: -------------------------------------------------------------------------------- 1 | import { extend } from 'vee-validate'; 2 | import PhoneNumber from 'awesome-phonenumber'; 3 | 4 | extend('phone', { 5 | message: 'This is not a valid phone number', 6 | validate (value) { 7 | const phone = new PhoneNumber(value); 8 | return phone.isValid(); 9 | } 10 | }); 11 | -------------------------------------------------------------------------------- /third_party/storefront/tests/e2e/pages/base.ts: -------------------------------------------------------------------------------- 1 | import Header from './components/header'; 2 | 3 | export default class Base { 4 | 5 | get path(): string { 6 | return '/'; 7 | } 8 | 9 | get header() { 10 | return Header; 11 | } 12 | 13 | visit(): Cypress.Chainable { 14 | return cy.visit(this.path); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /third_party/storefront/helpers/cacheControl.js: -------------------------------------------------------------------------------- 1 | const cacheControl = (values) => ({ res }) => { 2 | if (!process.server) return; 3 | 4 | const cacheControlValue = Object.entries(values) 5 | .map(([key, value]) => `${key}=${value}`) 6 | .join(','); 7 | 8 | res.setHeader('Cache-Control', cacheControlValue); 9 | }; 10 | 11 | export default cacheControl; 12 | -------------------------------------------------------------------------------- /third_party/storefront/tests/e2e/pages/utils/element.ts: -------------------------------------------------------------------------------- 1 | export function el(selector: string, children?: string): Cypress.Chainable { 2 | return children ? cy.get(`[data-e2e="${selector}"] ${children}`) : cy.get(`[data-e2e="${selector}"]`); 3 | } 4 | 5 | export function contains(selector: string, text: string): Cypress.Chainable { 6 | return cy.contains(`[data-e2e="${selector}"]`, text); 7 | } 8 | -------------------------------------------------------------------------------- /third_party/storefront/shims-webpack.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'webpack-hot-middleware' { 2 | const middleware: any; 3 | export interface Options { 4 | [proName: string]: any; 5 | } 6 | 7 | export interface ClientOptions { 8 | [proName: string]: any; 9 | } 10 | 11 | export interface MiddlewareOptions { 12 | [proName: string]: any; 13 | } 14 | 15 | export default middleware; 16 | } 17 | -------------------------------------------------------------------------------- /third_party/storefront/tests/e2e/fixtures/test-data/e2e-user-login.json: -------------------------------------------------------------------------------- 1 | { 2 | "Should successfully login": { 3 | "customer": { 4 | "password": "Vu3S70r3fr0n7!" 5 | } 6 | }, 7 | "Incorrect credentials - should display an error": { 8 | "customer": { 9 | "password": "Vu3S70r3fr0n7!" 10 | }, 11 | "errorMessage": "Account with the given credentials not found." 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /third_party/storefront/middleware/README.md: -------------------------------------------------------------------------------- 1 | # MIDDLEWARE 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains your application middleware. 6 | Middleware let you define custom functions that can be run before rendering either a page or a group of pages. 7 | 8 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/routing#middleware). 9 | -------------------------------------------------------------------------------- /third_party/storefront/tests/e2e/utils/data-generator.ts: -------------------------------------------------------------------------------- 1 | const generator = { 2 | get email(): string { 3 | return `cypress.${Date.now()}@vuestorefront.test`; 4 | }, 5 | 6 | get uuid(): string { 7 | return 'xxxxxxxx-xxxx-xxxx-xxxxxxxxxxxx'.replace(/[x]/g, () => { 8 | const random = Math.floor(Math.random() * 16); 9 | return random.toString(16); 10 | }); 11 | } 12 | 13 | }; 14 | 15 | export default generator; 16 | -------------------------------------------------------------------------------- /third_party/storefront/helpers/Checkout/getShippingMethodPrice.ts: -------------------------------------------------------------------------------- 1 | import { ShippingMethod } from '@vue-storefront/commercetools-api'; 2 | 3 | export default (shippingMethod: ShippingMethod, total: number) => { 4 | const centAmount = shippingMethod?.zoneRates[0].shippingRates[0].freeAbove?.centAmount; 5 | if (centAmount && total >= (centAmount / 100)) { 6 | return 0; 7 | } 8 | return shippingMethod.zoneRates[0].shippingRates[0].price.centAmount / 100; 9 | }; 10 | -------------------------------------------------------------------------------- /third_party/storefront/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleNameMapper: { 3 | '^@/(.*)$': '/$1', 4 | '^~/(.*)$': '/$1', 5 | '^vue$': 'vue/dist/vue.common.js' 6 | }, 7 | moduleFileExtensions: ['js', 'vue', 'json'], 8 | transform: { 9 | '^.+\\.js$': 'babel-jest', 10 | '.*\\.(vue)$': 'vue-jest' 11 | }, 12 | collectCoverage: true, 13 | collectCoverageFrom: [ 14 | '/components/**/*.vue', 15 | '/pages/**/*.vue' 16 | ] 17 | }; 18 | -------------------------------------------------------------------------------- /third_party/storefront/static/README.md: -------------------------------------------------------------------------------- 1 | # STATIC 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains your static files. 6 | Each file inside this directory is mapped to `/`. 7 | Thus you'd want to delete this README.md before deploying to production. 8 | 9 | Example: `/static/robots.txt` is mapped as `/robots.txt`. 10 | 11 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/assets#static). 12 | -------------------------------------------------------------------------------- /third_party/storefront/tests/e2e/fixtures/test-data/e2e-user-registration.json: -------------------------------------------------------------------------------- 1 | { 2 | "Should successfully register": { 3 | "customer": { 4 | "password": "Vu3S70r3fr0n7!", 5 | "firstName": "Jane", 6 | "lastName": "Doe" 7 | } 8 | }, 9 | "Existing user - should display an error": { 10 | "customer": { 11 | "password": "Vu3S70r3fr0n7!", 12 | "firstName": "Jane", 13 | "lastName": "Doe" 14 | }, 15 | "errorMessage": "There is already an existing customer with the email" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /third_party/storefront/tests/e2e/utils/network.ts: -------------------------------------------------------------------------------- 1 | import generator from '../utils/data-generator'; 2 | 3 | function _intercept(path: string, alias?: string) { 4 | const as = alias ?? generator.uuid; 5 | cy.intercept(path).as(as); 6 | return `@${as}`; 7 | } 8 | 9 | const intercept = { 10 | getProduct(as?: string): string { 11 | return _intercept('/getProduct', as); 12 | }, 13 | 14 | updateCartQuantity(as?: string): string { 15 | return _intercept('/updateCartQuantity', as); 16 | } 17 | }; 18 | 19 | export default intercept; 20 | -------------------------------------------------------------------------------- /third_party/storefront/themeConfig.js: -------------------------------------------------------------------------------- 1 | // default configuration for links and images 2 | export default { 3 | home: { 4 | bannerA: { 5 | link: "/", 6 | image: { 7 | mobile: "/homepage/bannerB.webp", 8 | desktop: "/homepage/bannerF.webp" 9 | } 10 | }, 11 | bannerB: { 12 | link: "/", 13 | image: "/homepage/bannerE.webp" 14 | }, 15 | bannerC: { 16 | link: "/", 17 | image: "/homepage/bannerC.webp" 18 | }, 19 | bannerD: { 20 | link: "/", 21 | image: "/homepage/bannerG.webp" 22 | } 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /third_party/storefront/tests/e2e/types/types.ts: -------------------------------------------------------------------------------- 1 | export type Address = { 2 | firstName?: string; 3 | lastName?: string; 4 | streetName?: string; 5 | apartment?: string; 6 | city?: string; 7 | state?: string; 8 | country?: string; 9 | postalCode?: string; 10 | phone?: string; 11 | } 12 | 13 | export type Customer = { 14 | firstName?: string; 15 | lastName?: string; 16 | email?: string; 17 | password?: string; 18 | address?: { 19 | shipping: Address, 20 | billing: Address 21 | } 22 | } 23 | 24 | export type Product = { 25 | sku: string; 26 | id: number; 27 | } 28 | -------------------------------------------------------------------------------- /import-data/tax-category.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "standard", 4 | "key": "standard", 5 | "rates": [ 6 | { 7 | "name": "10% incl.", 8 | "amount": 0.1, 9 | "includedInPrice": true, 10 | "country": "US", 11 | "state": "New York" 12 | }, 13 | { 14 | "name": "10% incl.", 15 | "amount": 0.1, 16 | "includedInPrice": true, 17 | "country": "US", 18 | "state": "California" 19 | }, 20 | { 21 | "name": "10% incl.", 22 | "amount": 0.1, 23 | "includedInPrice": true, 24 | "country": "US", 25 | "state": "Nevada" 26 | } 27 | ] 28 | } 29 | ] 30 | -------------------------------------------------------------------------------- /third_party/storefront/tests/e2e/fixtures/test-data/e2e-add-to-cart.json: -------------------------------------------------------------------------------- 1 | { 2 | "Should successfully add product to cart - Category grid view": { 3 | "product": { 4 | "category": "women", 5 | "name": "T-Shirt Moschino Cheap And Chic black" 6 | } 7 | }, 8 | "Should successfully add product to cart - Category list view": { 9 | "product": { 10 | "category": "men", 11 | "name": "Sweater Polo Ralph Lauren pink" 12 | } 13 | }, 14 | "Should successfully add product to cart - Product details page": { 15 | "product": { 16 | "name": "Lace up shoes Tods dark blue", 17 | "id": "eda439df-e655-493d-9fe9-b93c45144374", 18 | "slug": "tods-laceupshoes-C20RE0U820-darkblue" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /third_party/storefront/tests/e2e/cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrl": "http://localhost:3000", 3 | "fixturesFolder": "tests/e2e/fixtures", 4 | "integrationFolder": "tests/e2e/integration", 5 | "pluginsFile": "tests/e2e/plugins/index.js", 6 | "supportFile": "tests/e2e/support/index.js", 7 | "viewportHeight": 1080, 8 | "viewportWidth": 1920, 9 | "pageLoadTimeout": 180000, 10 | "screenshotOnRunFailure": true, 11 | "screenshotsFolder": "tests/e2e/report/assets/screenshots", 12 | "video": false, 13 | "reporter": "../../../../node_modules/mochawesome", 14 | "reporterOptions": { 15 | "reportDir": "tests/e2e/report", 16 | "reportFilename": "report", 17 | "overwrite": false, 18 | "html": false 19 | }, 20 | "retries": { 21 | "runMode": 2, 22 | "openMode": 0 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /third_party/storefront/middleware/checkout.js: -------------------------------------------------------------------------------- 1 | const canEnterPayment = cart => cart.shippingInfo && cart.shippingAddress; 2 | 3 | const canEnterReview = cart => Boolean(cart.billingAddress); 4 | 5 | export default async ({ app, $vsf }) => { 6 | const currentPath = app.context.route.fullPath.split('/checkout/')[1]; 7 | 8 | if (!currentPath) return; 9 | 10 | const { data } = await $vsf.$ct.api.getMe(); 11 | 12 | if (!data || !data.me.activeCart) return; 13 | const { activeCart } = data.me; 14 | 15 | switch (currentPath) { 16 | case 'billing': 17 | if (!canEnterPayment(activeCart)) { 18 | app.context.redirect('/'); 19 | } 20 | break; 21 | case 'payment': 22 | if (!canEnterReview(activeCart)) { 23 | app.context.redirect('/'); 24 | } 25 | break; 26 | } 27 | }; 28 | 29 | -------------------------------------------------------------------------------- /third_party/storefront/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | 16 | FROM node:12-alpine 17 | 18 | WORKDIR /usr/src/app 19 | 20 | COPY . ./ 21 | RUN npm install 22 | 23 | EXPOSE 3000 24 | 25 | RUN npm run build 26 | 27 | CMD [ "npm", "run", "start" ] -------------------------------------------------------------------------------- /third_party/storefront/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "lib": [ 7 | "esnext", 8 | "esnext.asynciterable", 9 | "dom" 10 | ], 11 | "esModuleInterop": true, 12 | "allowJs": true, 13 | "sourceMap": true, 14 | "strict": false, 15 | "noEmit": true, 16 | "baseUrl": ".", 17 | "paths": { 18 | "~/*": [ 19 | "./*" 20 | ], 21 | "@/*": [ 22 | "./*" 23 | ] 24 | }, 25 | "types": [ 26 | "@types/node", 27 | "@nuxt/types", 28 | "nuxt-i18n", 29 | "cypress" 30 | ] 31 | }, 32 | "exclude": [ 33 | "node_modules" 34 | ], 35 | "include": [ 36 | "tests/e2e/**/*.ts", 37 | "tests/e2e/plugins/index.js", 38 | "tests/e2e/support/index.js" 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /third_party/storefront/middleware.config.js: -------------------------------------------------------------------------------- 1 | const { 2 | CTP_PROJECT_KEY, 3 | CTP_CLIENT_SECRET, 4 | CTP_CLIENT_ID, 5 | CTP_AUTH_URL, 6 | CTP_API_URL, 7 | CTP_SCOPES 8 | } = process.env; 9 | 10 | module.exports = { 11 | integrations: { 12 | ct: { 13 | location: "@vue-storefront/commercetools-api/server", 14 | configuration: { 15 | api: { 16 | uri: `${CTP_API_URL}/${CTP_PROJECT_KEY}/graphql`, 17 | authHost: CTP_AUTH_URL, 18 | projectKey: CTP_PROJECT_KEY, 19 | clientId: CTP_CLIENT_ID, 20 | clientSecret: CTP_CLIENT_SECRET, 21 | scopes: [CTP_SCOPES] 22 | }, 23 | serverApi: { 24 | clientId: CTP_CLIENT_ID, 25 | clientSecret: CTP_CLIENT_SECRET, 26 | scopes: [CTP_SCOPES] 27 | }, 28 | currency: "USD", 29 | country: "US" 30 | } 31 | } 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /import-data/customers.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "customerNumber": "1", 4 | "email": "test.user@fake.com", 5 | "firstName": "Test", 6 | "lastName": "User", 7 | "title": "", 8 | "password": "p@ssword", 9 | "addresses": [ 10 | { 11 | "id": "PPM3YhMK", 12 | "title": "Home Address", 13 | "firstName": "Test", 14 | "lastName": "User", 15 | "streetName": "Fancy Street", 16 | "streetNumber": "123", 17 | "postalCode": "55555", 18 | "city": "New York", 19 | "state": "New York", 20 | "country": "US", 21 | "phone": "+15555555555", 22 | "mobile": "+15555555555", 23 | "email": "test.user@fake.com" 24 | } 25 | ], 26 | "shippingAddressIds": ["PPM3YhMK"], 27 | "billingAddressIds": ["PPM3YhMK"], 28 | "isEmailVerified": true, 29 | "key": "testKey", 30 | "dateOfBirth": "2001-01-01" 31 | } 32 | ] 33 | -------------------------------------------------------------------------------- /third_party/storefront/tests/e2e/fixtures/test-data/e2e-remove-from-cart.json: -------------------------------------------------------------------------------- 1 | { 2 | "Should remove all products from cart": { 3 | "product": { 4 | "name": "Shirt Aspesi white M", 5 | "id": 1, 6 | "sku": "M0E20000000ED0W", 7 | "quantity": 2, 8 | "size": 34 9 | } 10 | }, 11 | "Should remove single product from cart": { 12 | "products": [{ 13 | "name": "Shirt Aspesi white M", 14 | "id": 1, 15 | "sku": "M0E20000000ED0W", 16 | "quantity": 1, 17 | "size": 34 18 | }, 19 | { 20 | "name": "Shirt ”David” MU light blue", 21 | "id": 1, 22 | "sku": "M0E20000000DL5W", 23 | "quantity": 1, 24 | "size": 36 25 | } 26 | ], 27 | "productToRemove": { 28 | "name": "Shirt Aspesi white M" 29 | }, 30 | "expectedCart": [{ 31 | "name": "Shirt ”David” MU light blue" 32 | }] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /import-data/categories.csv: -------------------------------------------------------------------------------- 1 | key,externalId,name.de,slug.de,name.en,slug.en,name.it,slug.it,parentId,webImageUrl,iosImageUrl 2 | clothing,1,Clothing,clothing,Clothing,clothing,Clothing,clothing,,, 3 | accessories,2,Accessories,accessories,Accessories,accessories,Accessories,accessories,,, 4 | home,3,Home,home,Home,home,Home,home,,, 5 | beauty,4,Beauty,beauty,Beauty,beauty,Beauty,beauty,,, 6 | hair,5,Hair,beauty-hair,Hair,beauty-hair,Hair,beauty-hair,4,, 7 | kitchen,6,Kitchen,home-kitchen,Kitchen,home-kitchen,Kitchen,home-kitchen,3,, 8 | decor,7,Decor,home-decor,Decor,home-decor,Decor,home-decor,3,, 9 | tops,8,Tops,clothing-tops,Tops,clothing-tops,Tops,clothing-tops,1,, 10 | footwear,9,Footwear,clothing-footwear,Footwear,clothing-footwear,Footwear,clothing-footwear,1,, 11 | glasses,10,Glasses,accessories-glasses,Glasses,accessories-glasses,Glasses,accessories-glasses,2,, 12 | watches,11,Watches,accessories-watches,Watches,accessories-watches,Watches,accessories-watches,2,, -------------------------------------------------------------------------------- /third_party/storefront/README.md: -------------------------------------------------------------------------------- 1 | # Storefront 2 | 3 | The storefront was bootstrapped with the [Vue Storefront CLI](https://docs.vuestorefront.io/v2/general/installation.html). You will need to generate a mobile api client to be used from the storefront application. 4 | 5 | ## Create Storefront API Client 6 | 7 | 1. Go to your Merchant Center 8 | 2. Go to Settings -> Developer Settings -> Create new API client 9 | 3. Enter a name for your client (e.g. Storefront Client) 10 | 4. Choose the `Mobile & single-page application client` template 11 | 5. Click `Create API client` 12 | 6. At the bottom of the page, choose `Environment Variables (.env)` and copy the output 13 | 7. Create a file `.env` at the root of this repo and paste the text from the previous step 14 | 15 | ## Run Locally 16 | 17 | Install the NodeJS dependencies and run the development server. 18 | 19 | ```bash 20 | npm install 21 | npm run dev 22 | ``` 23 | 24 | Navigate to http://localhost:3000 25 | -------------------------------------------------------------------------------- /third_party/storefront/tests/e2e/plugins/index.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line spaced-comment 2 | /// 3 | // *********************************************************** 4 | // This example plugins/index.js can be used to load plugins 5 | // 6 | // You can change the location of this file or turn off loading 7 | // the plugins file with the 'pluginsFile' configuration option. 8 | // 9 | // You can read more here: 10 | // https://on.cypress.io/plugins-guide 11 | // *********************************************************** 12 | 13 | // This function is called when a project is opened or re-opened (e.g. due to 14 | // the project's config changing) 15 | 16 | const tagify = require('cypress-tags'); 17 | 18 | /** 19 | * @type {Cypress.PluginConfig} 20 | */ 21 | module.exports = (on, config) => { 22 | // `on` is used to hook into various events Cypress emits 23 | // `config` is the resolved Cypress config 24 | on('file:preprocessor', tagify(config)); 25 | }; 26 | -------------------------------------------------------------------------------- /third_party/storefront/static/icons/facebook.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /third_party/storefront/layouts/account.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 30 | 31 | 47 | -------------------------------------------------------------------------------- /third_party/storefront/tests/e2e/support/commands.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | // *********************************************** 3 | // This example commands.js shows you how to 4 | // create various custom commands and overwrite 5 | // existing commands. 6 | // 7 | // For more comprehensive examples of custom 8 | // commands please read more here: 9 | // https://on.cypress.io/custom-commands 10 | // *********************************************** 11 | // 12 | // 13 | // -- This is a parent command -- 14 | // Cypress.Commands.add("login", (email, password) => { ... }) 15 | // 16 | // 17 | // -- This is a child command -- 18 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 19 | // 20 | // 21 | // -- This is a dual command -- 22 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 23 | // 24 | // 25 | // -- This will overwrite an existing command -- 26 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 27 | -------------------------------------------------------------------------------- /third_party/storefront/tests/e2e/pages/product.ts: -------------------------------------------------------------------------------- 1 | import Base from './base'; 2 | import { el } from './utils/element'; 3 | 4 | export class Product extends Base { 5 | 6 | private _id: string; 7 | private _slug: string; 8 | 9 | constructor(id?: string, slug?: string) { 10 | super(); 11 | if (id) this.id = id; 12 | if (slug) this.slug = slug; 13 | } 14 | 15 | get id(): string { 16 | return this._id; 17 | } 18 | 19 | set id(id: string) { 20 | this._id = id; 21 | } 22 | 23 | get slug(): string { 24 | return this._slug; 25 | } 26 | 27 | set slug(slug: string) { 28 | this._slug = slug; 29 | } 30 | 31 | get path() { 32 | return `/p/${this.id}/${this.slug}`; 33 | } 34 | 35 | get addToCartButton(): Cypress.Chainable { 36 | return el('product_add-to-cart'); 37 | } 38 | 39 | get sizeSelect(): Cypress.Chainable { 40 | return el('size-select', 'select'); 41 | } 42 | 43 | get sizeOptions(): Cypress.Chainable { 44 | return el('size-select', 'select option'); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /third_party/storefront/tests/e2e/pages/components/header.ts: -------------------------------------------------------------------------------- 1 | import { el } from '../utils/element'; 2 | 3 | class Header { 4 | 5 | get account(): Cypress.Chainable { 6 | return el('app-header-account'); 7 | } 8 | 9 | get cart(): Cypress.Chainable { 10 | return el('app-header-cart'); 11 | } 12 | 13 | get categories(): Cypress.Chainable { 14 | return cy.get('[data-e2e*="app-header"]'); 15 | } 16 | 17 | get category() { 18 | return { 19 | women: () => el('app-header-url_women'), 20 | men: () => el('app-header-url_men') 21 | }; 22 | } 23 | 24 | openCart(): Cypress.Chainable { 25 | const click = $el => $el.click(); 26 | return this.cart.pipe(click).should(() => { 27 | expect(Cypress.$('[data-e2e="sidebar-cart"]')).to.exist; 28 | }); 29 | } 30 | 31 | openLoginModal(): Cypress.Chainable { 32 | const click = $el => $el.click(); 33 | return this.account.pipe(click).should(() => { 34 | expect(Cypress.$('[data-e2e="login-modal"]')).to.exist; 35 | }); 36 | } 37 | } 38 | 39 | export default new Header(); 40 | -------------------------------------------------------------------------------- /third_party/storefront/tests/e2e/pages/factory.ts: -------------------------------------------------------------------------------- 1 | import { Billing, Payment, Shipping, ThankYou } from './checkout'; 2 | import Cart from './components/cart-sidebar'; 3 | import LoginModal from './components/login-modal'; 4 | import Home from './home'; 5 | import { Sidebar } from './my-account'; 6 | import { Product } from './product'; 7 | import { Category } from './category'; 8 | 9 | const page = { 10 | get checkout() { 11 | return { 12 | shipping: new Shipping(), 13 | billing: new Billing(), 14 | payment: new Payment(), 15 | thankyou: new ThankYou() 16 | }; 17 | }, 18 | 19 | get components() { 20 | return { 21 | cart: Cart, 22 | loginModal: LoginModal 23 | }; 24 | }, 25 | 26 | get home() { 27 | return Home; 28 | }, 29 | 30 | get myAccount() { 31 | return { 32 | sidebar: new Sidebar() 33 | }; 34 | }, 35 | 36 | category(category?: string) { 37 | return new Category(category); 38 | }, 39 | 40 | product(id?: string, slug?: string) { 41 | return new Product(id, slug); 42 | } 43 | 44 | }; 45 | 46 | export default page; 47 | -------------------------------------------------------------------------------- /third_party/storefront/tests/e2e/fixtures/test-data/e2e-product-page.json: -------------------------------------------------------------------------------- 1 | { 2 | "Should contain all size options": { 3 | "product": { 4 | "id": "38143c0c-c9b0-448c-93cd-60eb90d8da57", 5 | "slug": "aspesi-shirt-h805-white", 6 | "attributes": { 7 | "size": [ 8 | "34", 9 | "36", 10 | "38", 11 | "40", 12 | "42", 13 | "44", 14 | "46", 15 | "48", 16 | "50", 17 | "52", 18 | "54", 19 | "56", 20 | "58", 21 | "70" 22 | ] 23 | } 24 | } 25 | }, 26 | "Should select correct size option": { 27 | "product": { 28 | "id": "38143c0c-c9b0-448c-93cd-60eb90d8da57", 29 | "slug": "aspesi-shirt-h805-white", 30 | "attributes": { 31 | "size": "58" 32 | } 33 | } 34 | }, 35 | "Should add correct variant to cart": { 36 | "product": { 37 | "id": "38143c0c-c9b0-448c-93cd-60eb90d8da57", 38 | "slug": "aspesi-shirt-h805-white", 39 | "attributes": { 40 | "size": "38", 41 | "color": "white" 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution; 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code Reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | 25 | ## Community Guidelines 26 | 27 | This project follows [Google's Open Source Community 28 | Guidelines](https://opensource.google/conduct/). 29 | -------------------------------------------------------------------------------- /third_party/storefront/tests/e2e/support/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | // *********************************************************** 3 | // This example support/index.js is processed and 4 | // loaded automatically before your test files. 5 | // 6 | // This is a great place to put global configuration and 7 | // behavior that modifies Cypress. 8 | // 9 | // You can change the location of this file or turn off 10 | // automatically serving support files with the 11 | // 'supportFile' configuration option. 12 | // 13 | // You can read more here: 14 | // https://on.cypress.io/configuration 15 | // *********************************************************** 16 | 17 | // Import commands.js using ES2015 syntax: 18 | import './commands.js'; 19 | import 'cypress-pipe'; 20 | 21 | // Alternatively you can use CommonJS syntax: 22 | // require('./commands') 23 | 24 | import addContext from 'mochawesome/addContext'; 25 | 26 | Cypress.on('test:after:run', (test, runnable) => { 27 | if (test.state === 'failed') { 28 | const screenshot = `assets/screenshots/${Cypress.spec.name}/${runnable.parent.title} -- ${test.title} (failed).png`; 29 | addContext({test}, { 30 | title: 'Screenshot', 31 | value: screenshot 32 | }); 33 | } 34 | }); 35 | 36 | -------------------------------------------------------------------------------- /third_party/storefront/pages/MyAccount/MyReviews.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 23 | 24 | 47 | -------------------------------------------------------------------------------- /third_party/storefront/pages/MyAccount/LoyaltyCard.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 23 | 24 | 48 | -------------------------------------------------------------------------------- /third_party/storefront/composables/useUiNotification/index.ts: -------------------------------------------------------------------------------- 1 | import { computed, reactive } from '@vue/composition-api'; 2 | 3 | interface UiNotification { 4 | message: string; 5 | action: { text: string; onClick: (...args: any) => void }; 6 | type: 'danger' | 'success' | 'info'; 7 | icon: string; 8 | persist: boolean; 9 | id: symbol; 10 | dismiss: () => void; 11 | } 12 | 13 | interface Notifications { 14 | notifications: Array; 15 | } 16 | 17 | const state = reactive({ 18 | notifications: [] 19 | }); 20 | const maxVisibleNotifications = 3; 21 | const timeToLive = 3000; 22 | 23 | const useUiNotification = () => { 24 | const send = (notification: UiNotification) => { 25 | const id = Symbol(); 26 | 27 | const dismiss = () => { 28 | const index = state.notifications.findIndex(notification => notification.id === id); 29 | 30 | if (index !== -1) state.notifications.splice(index, 1); 31 | }; 32 | 33 | const newNotification = { 34 | ...notification, 35 | id, 36 | dismiss 37 | }; 38 | 39 | state.notifications.push(newNotification); 40 | if (state.notifications.length > maxVisibleNotifications) state.notifications.shift(); 41 | 42 | if (!notification.persist) { 43 | setTimeout(dismiss, timeToLive); 44 | } 45 | }; 46 | 47 | return { 48 | send, 49 | notifications: computed(() => state.notifications) 50 | }; 51 | }; 52 | 53 | export default useUiNotification; 54 | -------------------------------------------------------------------------------- /third_party/storefront/tests/e2e/pages/components/login-modal.ts: -------------------------------------------------------------------------------- 1 | import { Customer } from '../../types/types'; 2 | import { el } from '../utils/element'; 3 | 4 | class LoginModal { 5 | 6 | get container(): Cypress.Chainable { 7 | return el('login-modal', '.sf-modal__container'); 8 | } 9 | 10 | get email(): Cypress.Chainable { 11 | return el('login-modal-email'); 12 | } 13 | 14 | get firstName(): Cypress.Chainable { 15 | return el('login-modal-firstName'); 16 | } 17 | 18 | get lastName(): Cypress.Chainable { 19 | return el('login-modal-lastName'); 20 | } 21 | 22 | get password(): Cypress.Chainable { 23 | return el('login-modal-password'); 24 | } 25 | 26 | get iWantToCreateAccountCheckbox(): Cypress.Chainable { 27 | return el('login-modal-create-account'); 28 | } 29 | 30 | get submitButton(): Cypress.Chainable { 31 | return el('login-modal-submit'); 32 | } 33 | 34 | get loginToAccountButton(): Cypress.Chainable { 35 | return el('login-modal-login-to-your-account'); 36 | } 37 | 38 | get loginBtn(): Cypress.Chainable { 39 | return el('login-modal-submit'); 40 | } 41 | 42 | fillForm(customer: Customer): void { 43 | if (customer.email) this.email.type(customer.email); 44 | if (customer.firstName) this.firstName.type(customer.firstName); 45 | if (customer.lastName) this.lastName.type(customer.lastName); 46 | if (customer.password) this.password.type(customer.password); 47 | } 48 | 49 | } 50 | 51 | export default new LoginModal(); 52 | -------------------------------------------------------------------------------- /import-data/shipping-methods.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Standard US", 4 | "key": "std-US", 5 | "localizedDescription": { 6 | "en": "Delivery in 5-6 working days" 7 | }, 8 | "taxCategory": { 9 | "typeId": "tax-category", 10 | "id": "US" 11 | }, 12 | "zoneRates": [ 13 | { 14 | "zone": { 15 | "typeId": "zone", 16 | "id": "United States" 17 | }, 18 | "shippingRates": [ 19 | { 20 | "price": { 21 | "currencyCode": "USD", 22 | "centAmount": 300 23 | }, 24 | "freeAbove": { 25 | "currencyCode": "USD", 26 | "centAmount": 20000 27 | }, 28 | "tiers": [] 29 | } 30 | ] 31 | } 32 | ], 33 | "isDefault": false 34 | }, 35 | { 36 | "name": "Express US", 37 | "key": "express-US", 38 | "localizedDescription": { 39 | "en": "Same day delivery" 40 | }, 41 | "taxCategory": { 42 | "typeId": "tax-category", 43 | "id": "US" 44 | }, 45 | "zoneRates": [ 46 | { 47 | "zone": { 48 | "typeId": "zone", 49 | "id": "United States" 50 | }, 51 | "shippingRates": [ 52 | { 53 | "price": { 54 | "currencyCode": "USD", 55 | "centAmount": 1000 56 | }, 57 | "tiers": [] 58 | } 59 | ] 60 | } 61 | ], 62 | "isDefault": false 63 | } 64 | ] 65 | -------------------------------------------------------------------------------- /third_party/storefront/components/UserBillingAddress.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 54 | 55 | 63 | -------------------------------------------------------------------------------- /third_party/storefront/components/UserShippingAddress.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 55 | 56 | 64 | -------------------------------------------------------------------------------- /third_party/storefront/tests/e2e/fixtures/test-data/e2e-update-cart.json: -------------------------------------------------------------------------------- 1 | { 2 | "Should increase product quantity": { 3 | "products": [{ 4 | "name": "Shirt Aspesi white M", 5 | "id": 1, 6 | "sku": "M0E20000000ED0W", 7 | "quantity": 2, 8 | "size": 34 9 | }, 10 | { 11 | "name": "Shirt ”David” MU light blue", 12 | "id": 1, 13 | "sku": "M0E20000000DL5W", 14 | "quantity": 2, 15 | "size": 36 16 | } 17 | ], 18 | "productToUpdate": { 19 | "name": "Shirt ”David” MU light blue" 20 | }, 21 | "expectedCart": [{ 22 | "name": "Shirt Aspesi white M", 23 | "quantity": 2 24 | }, 25 | { 26 | "name": "Shirt ”David” MU light blue", 27 | "quantity": 3 28 | } 29 | ] 30 | }, 31 | "Should decrease product quantity": { 32 | "products": [{ 33 | "name": "Shirt Aspesi white M", 34 | "id": 1, 35 | "sku": "M0E20000000ED0W", 36 | "quantity": 2, 37 | "size": 34 38 | }, 39 | { 40 | "name": "Shirt ”David” MU light blue", 41 | "id": 1, 42 | "sku": "M0E20000000DL5W", 43 | "quantity": 2, 44 | "size": 36 45 | } 46 | ], 47 | "productToUpdate": { 48 | "name": "Shirt ”David” MU light blue" 49 | }, 50 | "expectedCart": [{ 51 | "name": "Shirt Aspesi white M", 52 | "quantity": 2 53 | }, 54 | { 55 | "name": "Shirt ”David” MU light blue", 56 | "quantity": 1 57 | } 58 | ] 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /third_party/storefront/layouts/blank.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | 15 | 78 | -------------------------------------------------------------------------------- /third_party/storefront/tests/e2e/integration/e2e-add-to-cart.spec.ts: -------------------------------------------------------------------------------- 1 | import page from '../pages/factory'; 2 | 3 | context('Add product to cart', () => { 4 | beforeEach(function () { 5 | cy.fixture('test-data/e2e-add-to-cart').then((fixture) => { 6 | this.fixtures = { 7 | data: fixture 8 | }; 9 | }); 10 | }); 11 | it(['regression'], 'Should successfully add product to cart - Category grid view', function() { 12 | const data = this.fixtures.data[this.test.title]; 13 | const category = page.category(data.product.category); 14 | category.visit(); 15 | category.addToCart(data.product.name); 16 | category.header.openCart(); 17 | page.components.cart.product(data.product.name).should('be.visible'); 18 | }); 19 | 20 | it(['regression'], 'Should successfully add product to cart - Category list view', function() { 21 | const data = this.fixtures.data[this.test.title]; 22 | const category = page.category(data.product.category); 23 | category.visit(); 24 | category.changeView('list'); 25 | category.addToCart(data.product.name); 26 | category.header.openCart(); 27 | page.components.cart.product(data.product.name).should('be.visible'); 28 | }); 29 | 30 | it(['regression'], 'Should successfully add product to cart - Product details page', function() { 31 | const data = this.fixtures.data[this.test.title]; 32 | page.product(data.product.id, data.product.slug).visit(); 33 | page.product().addToCartButton.click(); 34 | page.product().header.openCart(); 35 | page.components.cart.product(data.product.name).should('be.visible'); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /third_party/storefront/tests/e2e/integration/e2e-user-login.spec.ts: -------------------------------------------------------------------------------- 1 | import requests from '../api/requests'; 2 | import page from '../pages/factory'; 3 | import generator from '../utils/data-generator'; 4 | 5 | context(['regression'], 'User login', () => { 6 | beforeEach(function () { 7 | cy.fixture('test-data/e2e-user-login').then((fixture) => { 8 | this.fixtures = { 9 | data: fixture 10 | }; 11 | }); 12 | cy.clearLocalStorage(); 13 | }); 14 | 15 | it('Should successfully login', function() { 16 | const data = this.fixtures.data[this.test.title]; 17 | data.customer.email = generator.email; 18 | requests.customerSignMeUp(data.customer).then(() => { 19 | cy.clearCookies(); 20 | }); 21 | page.home.visit(); 22 | page.home.header.openLoginModal(); 23 | page.components.loginModal.loginToAccountButton.click(); 24 | page.components.loginModal.fillForm(data.customer); 25 | page.components.loginModal.loginBtn.click(); 26 | page.components.loginModal.container.should('not.exist'); 27 | page.home.header.account.click(); 28 | page.myAccount.sidebar.heading.should('be.visible'); 29 | }); 30 | 31 | it('Incorrect credentials - should display an error', function () { 32 | const data = this.fixtures.data[this.test.title]; 33 | data.customer.email = generator.email; 34 | page.home.visit(); 35 | page.home.header.openLoginModal(); 36 | page.components.loginModal.loginToAccountButton.click(); 37 | page.components.loginModal.fillForm(data.customer); 38 | page.components.loginModal.loginBtn.click(); 39 | page.components.loginModal.container.contains(data.errorMessage).should('be.visible'); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /third_party/storefront/tests/e2e/integration/e2e-user-registration.spec.ts: -------------------------------------------------------------------------------- 1 | import page from '../pages/factory'; 2 | import requests from '../api/requests'; 3 | import generator from '../utils/data-generator'; 4 | 5 | context(['regression'], 'User registration', () => { 6 | beforeEach(function () { 7 | cy.fixture('test-data/e2e-user-registration').then((fixture) => { 8 | this.fixtures = { 9 | data: fixture 10 | }; 11 | }); 12 | }); 13 | 14 | it('Should successfully register', function () { 15 | const data = this.fixtures.data[this.test.title]; 16 | data.customer.email = generator.email; 17 | page.home.visit(); 18 | page.home.header.openLoginModal(); 19 | page.components.loginModal.fillForm(data.customer); 20 | page.components.loginModal.iWantToCreateAccountCheckbox.click(); 21 | page.components.loginModal.submitButton.click(); 22 | page.components.loginModal.container.should('not.exist'); 23 | page.home.header.account.click(); 24 | page.myAccount.sidebar.heading.should('be.visible'); 25 | }); 26 | 27 | it('Existing user - should display an error', function () { 28 | const data = this.fixtures.data[this.test.title]; 29 | data.customer.email = generator.email; 30 | requests.customerSignMeUp(data.customer).then(() => { 31 | cy.clearCookies(); 32 | }); 33 | page.home.visit(); 34 | page.home.header.openLoginModal(); 35 | page.components.loginModal.fillForm(data.customer); 36 | page.components.loginModal.iWantToCreateAccountCheckbox.click(); 37 | page.components.loginModal.submitButton.click(); 38 | page.components.loginModal.container.contains(`${data.errorMessage} '"${data.customer.email}"'`).should('be.visible'); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /third_party/storefront/tests/e2e/pages/category.ts: -------------------------------------------------------------------------------- 1 | import { el } from './utils/element'; 2 | import Base from './base'; 3 | 4 | type View = 'tiles' | 'list' 5 | 6 | export class Category extends Base { 7 | 8 | private readonly _category: string; 9 | private _view: View = 'tiles'; 10 | 11 | constructor(category?: string) { 12 | super(); 13 | if (category) this._category = category; 14 | } 15 | 16 | set view(view: View) { 17 | this._view = view; 18 | } 19 | 20 | get view(): View { 21 | return this._view; 22 | } 23 | 24 | get category(): string { 25 | return this._category; 26 | } 27 | 28 | get path(): string { 29 | return `/c/${this.category}`; 30 | } 31 | 32 | get products(): Cypress.Chainable { 33 | return el('category-product-card', 'a'); 34 | } 35 | 36 | get selectors(): { [key: string]: { [key in View]: string } } { 37 | return { 38 | productCard: { 39 | tiles: '.sf-product-card', 40 | list: '.sf-product-card-horizontal' 41 | }, 42 | addToCardButton: { 43 | tiles: '.sf-product-card__add-button', 44 | list: '.sf-add-to-cart__button' 45 | } 46 | }; 47 | } 48 | 49 | viewIcon(view: View): Cypress.Chainable { 50 | return el(`${view}-icon`); 51 | } 52 | 53 | changeView(view: View): Cypress.Chainable { 54 | this.view = view; 55 | return this.viewIcon(view).click(); 56 | } 57 | 58 | product(name: string): Cypress.Chainable { 59 | return this.products.contains(name); 60 | } 61 | 62 | addToCart(name: string): Cypress.Chainable { 63 | return this.product(name).parents(this.selectors.productCard[this.view]) 64 | .find(this.selectors.addToCardButton[this.view]) 65 | .click(); 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /third_party/storefront/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "headless-store", 3 | "description": "Sample Headless Commerce Store", 4 | "version": "1.3.0", 5 | "publishConfig": { 6 | "access": "public" 7 | }, 8 | "scripts": { 9 | "dev": "nuxt", 10 | "build": "nuxt build -m", 11 | "build:analyze": "nuxt build -a -m", 12 | "start": "nuxt start", 13 | "test": "jest", 14 | "test:e2e": "cypress open --config-file tests/e2e/cypress.json", 15 | "test:e2e:hl": "cypress run --headless --config-file tests/e2e/cypress.json", 16 | "test:e2e:generate:report": "yarn -s mochawesome-merge \"tests/e2e/report/*.json\" > \"tests/e2e/report.json\" && yarn -s marge tests/e2e/report.json -o \"tests/e2e/report\"" 17 | }, 18 | "dependencies": { 19 | "@nuxtjs/pwa": "^3.2.2", 20 | "@storefront-ui/vue": "0.10.3", 21 | "@vue-storefront/commercetools": "~1.3.0", 22 | "@vue-storefront/middleware": "~2.4.0", 23 | "@vue-storefront/nuxt": "~2.4.0", 24 | "@vue-storefront/nuxt-theme": "~2.4.0", 25 | "awesome-phonenumber": "^2.51.2", 26 | "cookie-universal-nuxt": "^2.1.3", 27 | "core-js": "^2.6.5", 28 | "nuxt": "^2.13.3", 29 | "nuxt-i18n": "^6.5.0", 30 | "vee-validate": "^3.2.3", 31 | "vue-scrollto": "^2.17.1" 32 | }, 33 | "devDependencies": { 34 | "@nuxt/types": "^0.7.9", 35 | "@vue/test-utils": "^1.0.0-beta.27", 36 | "babel-jest": "^24.1.0", 37 | "cypress": "^6.6.0", 38 | "cypress-pipe": "^2.0.0", 39 | "cypress-tags": "^0.0.20", 40 | "jest": "^24.1.0", 41 | "mochawesome": "^6.2.2", 42 | "mochawesome-merge": "^4.2.0", 43 | "mochawesome-report-generator": "^5.2.0", 44 | "vue-jest": "^4.0.0-0" 45 | }, 46 | "engines": { 47 | "node": ">=12 <=14", 48 | "npm": "<=6" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /third_party/storefront/tests/e2e/integration/e2e-remove-from-cart.spec.ts: -------------------------------------------------------------------------------- 1 | import requests, { CreateCartResponse } from '../api/requests'; 2 | import page from '../pages/factory'; 3 | 4 | context('Remove from cart', () => { 5 | beforeEach(function () { 6 | cy.fixture('test-data/e2e-remove-from-cart').then((fixture) => { 7 | this.fixtures = { 8 | data: fixture 9 | }; 10 | }); 11 | }); 12 | 13 | it('Should remove all products from cart', function () { 14 | const data = this.fixtures.data[this.test.title]; 15 | requests.createCart().then((response: CreateCartResponse) => { 16 | requests.addToCart(response.body.data.cart.id, data.product, data.product.quantity); 17 | }); 18 | page.home.visit(); 19 | page.home.header.openCart(); 20 | page.components.cart.product(data.product.name).should('be.visible'); 21 | page.components.cart.removeProduct(data.product.name); 22 | page.components.cart.product(data.product.name).should('not.exist'); 23 | page.components.cart.yourCartIsEmptyHeading.should('be.visible'); 24 | }); 25 | 26 | it('Should remove single product from cart', function () { 27 | const data = this.fixtures.data[this.test.title]; 28 | requests.createCart().then((response: CreateCartResponse) => { 29 | data.products.forEach(product => { 30 | requests.addToCart(response.body.data.cart.id, product, product.quantity); 31 | }); 32 | }); 33 | page.home.visit(); 34 | page.home.header.openCart(); 35 | page.components.cart.product(data.productToRemove.name).should('be.visible'); 36 | page.components.cart.removeProduct(data.productToRemove.name); 37 | page.components.cart.product(data.productToRemove.name).should('not.exist'); 38 | data.expectedCart.forEach(product => { 39 | page.components.cart.product(product.name).should('be.visible'); 40 | }); 41 | }); 42 | 43 | }); 44 | -------------------------------------------------------------------------------- /third_party/storefront/.dockerignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # TypeScript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env* 61 | set_env.sh 62 | 63 | # parcel-bundler cache (https://parceljs.org/) 64 | .cache 65 | 66 | # next.js build output 67 | .next 68 | 69 | # nuxt.js build output 70 | .nuxt 71 | 72 | # theme 73 | _theme 74 | 75 | # Nuxt generate 76 | dist 77 | 78 | # vuepress build output 79 | .vuepress/dist 80 | 81 | # Serverless directories 82 | .serverless 83 | 84 | # IDE / Editor 85 | .idea 86 | 87 | # Service worker 88 | sw.* 89 | 90 | # Mac OSX 91 | .DS_Store 92 | 93 | # Vim swap files 94 | *.swp 95 | 96 | version 97 | 98 | # e2e reports 99 | tests/e2e/report.json 100 | tests/e2e/report 101 | -------------------------------------------------------------------------------- /third_party/storefront/.gcloudignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # TypeScript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env* 61 | set_env.sh 62 | 63 | # parcel-bundler cache (https://parceljs.org/) 64 | .cache 65 | 66 | # next.js build output 67 | .next 68 | 69 | # nuxt.js build output 70 | .nuxt 71 | 72 | # theme 73 | _theme 74 | 75 | # Nuxt generate 76 | dist 77 | 78 | # vuepress build output 79 | .vuepress/dist 80 | 81 | # Serverless directories 82 | .serverless 83 | 84 | # IDE / Editor 85 | .idea 86 | 87 | # Service worker 88 | sw.* 89 | 90 | # Mac OSX 91 | .DS_Store 92 | 93 | # Vim swap files 94 | *.swp 95 | 96 | version 97 | 98 | # e2e reports 99 | tests/e2e/report.json 100 | tests/e2e/report 101 | -------------------------------------------------------------------------------- /third_party/storefront/.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # TypeScript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env* 61 | set_env.sh 62 | 63 | # parcel-bundler cache (https://parceljs.org/) 64 | .cache 65 | 66 | # next.js build output 67 | .next 68 | 69 | # nuxt.js build output 70 | .nuxt 71 | 72 | # theme 73 | _theme 74 | 75 | # Nuxt generate 76 | dist 77 | 78 | # vuepress build output 79 | .vuepress/dist 80 | 81 | # Serverless directories 82 | .serverless 83 | 84 | # IDE / Editor 85 | .idea 86 | 87 | # Service worker 88 | sw.* 89 | 90 | # Mac OSX 91 | .DS_Store 92 | 93 | # Vim swap files 94 | *.swp 95 | 96 | version 97 | 98 | # e2e reports 99 | tests/e2e/report.json 100 | tests/e2e/report 101 | -------------------------------------------------------------------------------- /third_party/storefront/components/RelatedProducts.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 51 | 52 | 68 | -------------------------------------------------------------------------------- /third_party/storefront/components/Checkout/VsfPaymentProvider.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 62 | 63 | 76 | -------------------------------------------------------------------------------- /third_party/storefront/tests/e2e/fixtures/test-data/e2e-place-order.json: -------------------------------------------------------------------------------- 1 | { 2 | "Should successfully place an order as a guest": { 3 | "customer": { 4 | "address": { 5 | "shipping": { 6 | "firstName": "John", 7 | "lastName": "Doe", 8 | "streetName": "1 VueStorefront Rd.", 9 | "apartment": "23", 10 | "city": "Los Angeles", 11 | "state": "California", 12 | "country": "United States", 13 | "postalCode": "90210", 14 | "phone": "+12345678910" 15 | }, 16 | "billing": { 17 | "firstName": "John", 18 | "lastName": "Doe", 19 | "streetName": "1 VueStorefront Rd.", 20 | "apartment": "23", 21 | "city": "Los Angeles", 22 | "state": "California", 23 | "country": "United States", 24 | "postalCode": "90210", 25 | "phone": "+12345678910" 26 | } 27 | } 28 | } 29 | }, 30 | "Should successfully place an order as a registered customer": { 31 | "customer": { 32 | "firstName": "John", 33 | "lastName": "Doe", 34 | "password": "P@ssw0rd", 35 | "address": { 36 | "shipping": { 37 | "firstName": "John", 38 | "lastName": "Doe", 39 | "streetName": "1 VueStorefront Rd.", 40 | "apartment": "23", 41 | "city": "Los Angeles", 42 | "state": "California", 43 | "country": "United States", 44 | "postalCode": "90210", 45 | "phone": "+12345678910" 46 | }, 47 | "billing": { 48 | "firstName": "John", 49 | "lastName": "Doe", 50 | "streetName": "1 VueStorefront Rd.", 51 | "apartment": "23", 52 | "city": "Los Angeles", 53 | "state": "California", 54 | "country": "United States", 55 | "postalCode": "90210", 56 | "phone": "+12345678910" 57 | } 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /third_party/storefront/tests/e2e/integration/e2e-product-page.spec.ts: -------------------------------------------------------------------------------- 1 | import page from '../pages/factory'; 2 | import intercept from '../utils/network'; 3 | 4 | context(['regression'], 'Product page', () => { 5 | beforeEach(function () { 6 | cy.fixture('test-data/e2e-product-page').then((fixture) => { 7 | this.fixtures = { 8 | data: fixture 9 | }; 10 | }); 11 | cy.clearLocalStorage(); 12 | }); 13 | 14 | it('Should contain all size options', function () { 15 | const data = this.fixtures.data[this.test.title]; 16 | page.product(data.product.id, data.product.slug).visit(); 17 | page.product().sizeOptions.then(options => { 18 | const productSizes = [...options].map(option => option.value); 19 | expect(productSizes).to.deep.eq(data.product.attributes.size); 20 | }); 21 | }); 22 | 23 | it('Should select correct size option', function () { 24 | const data = this.fixtures.data[this.test.title]; 25 | page.product(data.product.id, data.product.slug).visit(); 26 | page.product().sizeSelect.select(data.product.attributes.size); 27 | cy.url().should('contain', `size=${data.product.attributes.size}`); 28 | page.product().sizeSelect.should('have.value', data.product.attributes.size); 29 | }); 30 | 31 | it('Should add correct variant to cart', function() { 32 | const data = this.fixtures.data[this.test.title]; 33 | const getProductReq = intercept.getProduct(); 34 | page.product(data.product.id, data.product.slug).visit(); 35 | page.product().sizeSelect.select(data.product.attributes.size).then(() => { 36 | cy.wait(getProductReq); 37 | }); 38 | page.product().addToCartButton.click(); 39 | page.product().header.openCart(); 40 | page.components.cart.productProperties.should('be.visible').then(() => { 41 | page.components.cart.product().each((product) => { 42 | page.components.cart.productSizeProperty(product).should('contain', data.product.attributes.size); 43 | page.components.cart.productColorProperty(product).should('contain', data.product.attributes.color); 44 | }); 45 | }); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /third_party/storefront/tests/e2e/pages/components/cart-sidebar.ts: -------------------------------------------------------------------------------- 1 | import { el } from '../utils/element'; 2 | 3 | class Cart { 4 | 5 | product(name?: string): Cypress.Chainable { 6 | const product = el('collected-product'); 7 | return name === undefined ? product : product.contains(name).parents('[data-e2e="collected-product"]'); 8 | } 9 | 10 | get productName(): Cypress.Chainable { 11 | return this.product().find('.sf-collected-product__title'); 12 | } 13 | 14 | get goToCheckoutButton(): Cypress.Chainable { 15 | return el('go-to-checkout-btn'); 16 | } 17 | 18 | get productProperties(): Cypress.Chainable { 19 | return this.product().find('.collected-product__properties'); 20 | } 21 | 22 | get totalItems(): Cypress.Chainable { 23 | return el('sidebar-cart', '.cart-summary .sf-property__value'); 24 | } 25 | 26 | get yourCartIsEmptyHeading(): Cypress.Chainable { 27 | return el('sidebar-cart', 'h2').contains('Your cart is empty'); 28 | } 29 | 30 | propertyValue(collectedProduct: JQuery): Cypress.Chainable { 31 | return cy.wrap(collectedProduct).find('.sf-property__value'); 32 | } 33 | 34 | productSizeProperty(collectedProduct: JQuery): Cypress.Chainable { 35 | return this.propertyValue(collectedProduct).eq(0); 36 | } 37 | 38 | productColorProperty(collectedProduct: JQuery): Cypress.Chainable { 39 | return this.propertyValue(collectedProduct).eq(1); 40 | } 41 | 42 | removeProductButton(name?: string): Cypress.Chainable { 43 | return this.product(name).find('.sf-collected-product__remove'); 44 | } 45 | 46 | removeProduct(name?: string): Cypress.Chainable { 47 | return this.removeProductButton(name).first().click(); 48 | } 49 | 50 | increaseQtyButton(name?: string): Cypress.Chainable { 51 | return this.product(name).find('button').contains('+'); 52 | } 53 | 54 | decreaseQtyButton(name?: string): Cypress.Chainable { 55 | return this.product(name).find('button').contains('−'); 56 | } 57 | 58 | quantity(name?: string): Cypress.Chainable { 59 | return this.product(name).find('input'); 60 | } 61 | } 62 | 63 | export default new Cart(); 64 | -------------------------------------------------------------------------------- /third_party/storefront/components/MobileStoreBanner.vue: -------------------------------------------------------------------------------- 1 | 34 | 49 | 82 | -------------------------------------------------------------------------------- /third_party/storefront/composables/useUiState.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import VueCompositionAPI, { reactive, computed } from '@vue/composition-api'; 3 | 4 | // We need to register it again because of Vue instance instantiation issues 5 | Vue.use(VueCompositionAPI); 6 | 7 | const state = reactive({ 8 | isCartSidebarOpen: false, 9 | isWishlistSidebarOpen: false, 10 | isLoginModalOpen: false, 11 | isCategoryGridView: true, 12 | isFilterSidebarOpen: false, 13 | isMobileMenuOpen: false 14 | }); 15 | 16 | const useUiState = () => { 17 | const isMobileMenuOpen = computed(() => state.isMobileMenuOpen); 18 | const toggleMobileMenu = () => { 19 | state.isMobileMenuOpen = !state.isMobileMenuOpen; 20 | }; 21 | 22 | const isCartSidebarOpen = computed(() => state.isCartSidebarOpen); 23 | const toggleCartSidebar = () => { 24 | if (state.isMobileMenuOpen) toggleMobileMenu(); 25 | state.isCartSidebarOpen = !state.isCartSidebarOpen; 26 | }; 27 | 28 | const isWishlistSidebarOpen = computed(() => state.isWishlistSidebarOpen); 29 | const toggleWishlistSidebar = () => { 30 | if (state.isMobileMenuOpen) toggleMobileMenu(); 31 | state.isWishlistSidebarOpen = !state.isWishlistSidebarOpen; 32 | }; 33 | 34 | const isLoginModalOpen = computed(() => state.isLoginModalOpen); 35 | const toggleLoginModal = () => { 36 | if (state.isMobileMenuOpen) toggleMobileMenu(); 37 | state.isLoginModalOpen = !state.isLoginModalOpen; 38 | }; 39 | 40 | const isCategoryGridView = computed(() => state.isCategoryGridView); 41 | const changeToCategoryGridView = () => { 42 | state.isCategoryGridView = true; 43 | }; 44 | const changeToCategoryListView = () => { 45 | state.isCategoryGridView = false; 46 | }; 47 | 48 | const isFilterSidebarOpen = computed(() => state.isFilterSidebarOpen); 49 | const toggleFilterSidebar = () => { 50 | state.isFilterSidebarOpen = !state.isFilterSidebarOpen; 51 | }; 52 | 53 | return { 54 | isCartSidebarOpen, 55 | isWishlistSidebarOpen, 56 | isLoginModalOpen, 57 | isCategoryGridView, 58 | isFilterSidebarOpen, 59 | isMobileMenuOpen, 60 | toggleCartSidebar, 61 | toggleWishlistSidebar, 62 | toggleLoginModal, 63 | changeToCategoryGridView, 64 | changeToCategoryListView, 65 | toggleFilterSidebar, 66 | toggleMobileMenu 67 | }; 68 | }; 69 | 70 | export default useUiState; 71 | -------------------------------------------------------------------------------- /third_party/storefront/components/BottomNavigation.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 66 | 71 | -------------------------------------------------------------------------------- /third_party/storefront/components/HeaderNavigation.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 74 | 75 | 90 | -------------------------------------------------------------------------------- /third_party/storefront/tests/e2e/integration/e2e-update-cart.spec.ts: -------------------------------------------------------------------------------- 1 | import requests, { CreateCartResponse } from '../api/requests'; 2 | import page from '../pages/factory'; 3 | import intercept from '../utils/network'; 4 | 5 | context(['regression'], 'Update cart', () => { 6 | beforeEach(function () { 7 | cy.fixture('test-data/e2e-update-cart').then((fixture) => { 8 | this.fixtures = { 9 | data: fixture 10 | }; 11 | }); 12 | }); 13 | 14 | it('Should increase product quantity', function () { 15 | const data = this.fixtures.data[this.test.title]; 16 | requests.createCart().then((response: CreateCartResponse) => { 17 | data.products.forEach(product => { 18 | requests.addToCart(response.body.data.cart.id, product, product.quantity); 19 | }); 20 | }); 21 | page.home.visit(); 22 | page.home.header.openCart(); 23 | page.components.cart.product(data.productToUpdate.name).should('be.visible'); 24 | const updateCartRequest = intercept.updateCartQuantity(); 25 | page.components.cart.increaseQtyButton(data.productToUpdate.name).click().then(() => { 26 | cy.wait(updateCartRequest); 27 | }); 28 | page.components.cart.productName.each((name, index) => { 29 | cy.wrap(name).should('contain', data.expectedCart[index].name); 30 | }); 31 | page.components.cart.quantity().each((input, index) => { 32 | cy.wrap(input).should('have.value', data.expectedCart[index].quantity); 33 | }); 34 | }); 35 | 36 | it('Should decrease product quantity', function () { 37 | const data = this.fixtures.data[this.test.title]; 38 | requests.createCart().then((response: CreateCartResponse) => { 39 | data.products.forEach(product => { 40 | requests.addToCart(response.body.data.cart.id, product, product.quantity); 41 | }); 42 | }); 43 | page.home.visit(); 44 | page.home.header.openCart(); 45 | page.components.cart.product(data.productToUpdate.name).should('be.visible'); 46 | const updateCartRequest = intercept.updateCartQuantity(); 47 | page.components.cart.decreaseQtyButton(data.productToUpdate.name).click().then(() => { 48 | cy.wait(updateCartRequest); 49 | }); 50 | page.components.cart.productName.each((name, index) => { 51 | cy.wrap(name).should('contain', data.expectedCart[index].name); 52 | }); 53 | page.components.cart.quantity().each((input, index) => { 54 | cy.wrap(input).should('have.value', data.expectedCart[index].quantity); 55 | }); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /third_party/storefront/tests/e2e/fixtures/test-data/e2e-checkout-order-summary.json: -------------------------------------------------------------------------------- 1 | { 2 | "Should contain correct data in Order Summary": { 3 | "products": [{ 4 | "name": "Shirt Aspesi white M", 5 | "id": 1, 6 | "sku": "M0E20000000ED0W", 7 | "size": 34, 8 | "quantity": 3 9 | }, 10 | { 11 | "name": "Shirt Aspesi white M", 12 | "id": 1, 13 | "sku": "M0E20000000ED0X", 14 | "size": 36, 15 | "quantity": 1 16 | }, 17 | { 18 | "name": "T-Shirt Moschino Cheap And Chic black", 19 | "id": 1, 20 | "sku": "M0E20000000DLPH", 21 | "size": 34, 22 | "quantity": 1 23 | } 24 | ], 25 | "customer": { 26 | "address": { 27 | "shipping": { 28 | "firstName": "Jane", 29 | "lastName": "Doe", 30 | "streetName": "Vuestorefront Rd.", 31 | "apartment": "13/37", 32 | "city": "Los Angeles", 33 | "state": "California", 34 | "country": "US", 35 | "postalCode": "90001", 36 | "phone": "+12345678910" 37 | }, 38 | "billing": { 39 | "firstName": "Jane", 40 | "lastName": "Doe", 41 | "streetName": "Vuestorefront Rd.", 42 | "apartment": "13/37", 43 | "city": "Los Angeles", 44 | "state": "California", 45 | "country": "US", 46 | "postalCode": "90001", 47 | "phone": "+12345678910" 48 | } 49 | } 50 | }, 51 | "shippingMethod": "Express US", 52 | "expectedCartSummary": { 53 | "products": [{ 54 | "name": "Shirt Aspesi white M", 55 | "id": 1, 56 | "sku": "M0E20000000ED0W", 57 | "size": 34, 58 | "color": "white", 59 | "quantity": 3, 60 | "amount": "$359.10" 61 | }, 62 | { 63 | "name": "Shirt Aspesi white M", 64 | "id": 1, 65 | "sku": "M0E20000000ED0X", 66 | "size": 36, 67 | "color": "white", 68 | "quantity": 1, 69 | "amount": "$166.25" 70 | }, 71 | { 72 | "name": "T-Shirt Moschino Cheap And Chic black", 73 | "id": 1, 74 | "sku": "M0E20000000DLPH", 75 | "size": 34, 76 | "color": "black", 77 | "quantity": 1, 78 | "amount": "$372.50" 79 | } 80 | ], 81 | "discountedPrice": "$1,037.50", 82 | "specialPrice": "$897.85", 83 | "totalPrice": "$907.85" 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /third_party/storefront/components/Notification.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 39 | 40 | 95 | -------------------------------------------------------------------------------- /third_party/storefront/components/Checkout/UserShippingAddresses.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 72 | 73 | 90 | -------------------------------------------------------------------------------- /import-data/products.csv: -------------------------------------------------------------------------------- 1 | "productType","variantId","sku","prices","tax","categories","images","name.en","description.en","slug.en","metaTitle.en","metaDescription.en","metaKeywords.en" 2 | "main","1","OLJCESPC7Z","US-USD 1999","standard","accessories;accessories>glasses","/img/products/sunglasses.jpg","Sunglasses","Add a modern touch to your outfits with these sleek aviator sunglasses.","sunglasses","Sunglasses","Add a modern touch to your outfits with these sleek aviator sunglasses.","sunglasses" 3 | "main","1","66VCHSJNUP","US-USD 1899","standard","clothing;clothing>tops","/img/products/tank-top.jpg","Tank Top","Perfectly cropped cotton tank with a scooped neckline.","tank-top","Tank Top","Perfectly cropped cotton tank with a scooped neckline.","tank-top" 4 | "main","1","1YMWWN1N4O","US-USD 10999","standard","accessories;accessories>watches","/img/products/watch.jpg","Watch","This gold-tone stainless steel watch will work with most of your outfits.","watch","Watch","This gold-tone stainless steel watch will work with most of your outfits.","watch" 5 | "main","1","L9ECAV7KIM","US-USD 8999","standard","clothing;clothing>footwear","/img/products/loafers.jpg","Loafers","A neat addition to your summer wardrobe.","loafers","Loafers","A neat addition to your summer wardrobe.","loafers" 6 | "main","1","2ZYFJ3GM2N","US-USD 2499","standard","beauty;beauty>hair","/img/products/hairdryer.jpg","Hairdryer","This lightweight hairdryer has 3 heat and speed settings. It's perfect for travel.","hairdryer","Hairdryer","This lightweight hairdryer has 3 heat and speed settings. It's perfect for travel.","hairdryer" 7 | "main","1","0PUK6V6EV0","US-USD 1899","standard","home;home>decor","/img/products/candle-holder.jpg","Candle Holder","This small but intricate candle holder is an excellent gift.","candle-holder","Candle Holder","This small but intricate candle holder is an excellent gift.","candle-holder" 8 | "main","1","LS4PSXUNUM","US-USD 1899","standard","home;home>kitchen","/img/products/salt-and-pepper-shakers.jpg","Salt & Pepper Shakers","Add some flavor to your kitchen.","salt-and-pepper-shakers","Salt & Pepper Shakers","Add some flavor to your kitchen.","salt-and-pepper-shakers" 9 | "main","1","9SIQT8TOJO","US-USD 549","standard","home;home>kitchen","/img/products/bamboo-glass-jar.jpg","Bamboo Glass Jar","This bamboo glass jar can hold 57 oz (1.7 l) and is perfect for any kitchen.","bamboo-glass-jar","Bamboo Glass Jar","This bamboo glass jar can hold 57 oz (1.7 l) and is perfect for any kitchen.","bamboo-glass-jar" 10 | "main","1","6E92ZMYYFZ","US-USD 899","standard","home;home>kitchen","/img/products/mug.jpg","Mug","A simple mug with a mustard interior.","mug","Mug","A simple mug with a mustard interior.","mug" -------------------------------------------------------------------------------- /third_party/storefront/layouts/error.vue: -------------------------------------------------------------------------------- 1 | 24 | 34 | 92 | -------------------------------------------------------------------------------- /third_party/storefront/pages/MyAccount/MyNewsletter.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 54 | 55 | 100 | -------------------------------------------------------------------------------- /third_party/storefront/layouts/default.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 49 | 50 | 113 | -------------------------------------------------------------------------------- /third_party/storefront/components/LocaleSelector.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 59 | 60 | 101 | -------------------------------------------------------------------------------- /third_party/storefront/pages/Checkout.vue: -------------------------------------------------------------------------------- 1 | 32 | 71 | 72 | 108 | -------------------------------------------------------------------------------- /third_party/storefront/tests/e2e/fixtures/test-data/e2e-carts-merging.json: -------------------------------------------------------------------------------- 1 | { 2 | "Should merge guest cart with registered customer cart": { 3 | "customer": { 4 | "password": "Vu3S70r3fr0n7!" 5 | }, 6 | "products": [{ 7 | "name": "Shirt Aspesi white M", 8 | "id": 1, 9 | "sku": "M0E20000000ED0W", 10 | "quantity": 1, 11 | "size": 34 12 | }, 13 | { 14 | "name": "Shirt Aspesi white M", 15 | "id": 2, 16 | "sku": "M0E20000000ED0X", 17 | "quantity": 1, 18 | "size": 36 19 | }, 20 | { 21 | "name": "Shirt ”David” MU light blue", 22 | "id": 1, 23 | "sku": "M0E20000000DL5W", 24 | "quantity": 2, 25 | "size": 36 26 | } 27 | ], 28 | "expectedCart": [{ 29 | "name": "Shirt Aspesi white M", 30 | "quantity": 1, 31 | "size": 34, 32 | "color": "white" 33 | }, 34 | { 35 | "name": "Shirt Aspesi white M", 36 | "quantity": 1, 37 | "size": 36, 38 | "color": "white" 39 | }, 40 | { 41 | "name": "Shirt ”David” MU light blue", 42 | "quantity": 2, 43 | "size": 36, 44 | "color": "blue" 45 | } 46 | ] 47 | }, 48 | 49 | "Should merge guest cart with registered customer cart - products already in cart": { 50 | "customer": { 51 | "password": "Vu3S70r3fr0n7!" 52 | }, 53 | "products": { 54 | "guest": [{ 55 | "name": "Shirt Aspesi white M", 56 | "id": 1, 57 | "sku": "M0E20000000ED0W", 58 | "quantity": 1, 59 | "size": 34 60 | }, 61 | { 62 | "name": "Shirt Aspesi white M", 63 | "id": 2, 64 | "sku": "M0E20000000ED0X", 65 | "quantity": 1, 66 | "size": 36 67 | }, 68 | { 69 | "name": "Shirt ”David” MU light blue", 70 | "id": 1, 71 | "sku": "M0E20000000DL5W", 72 | "quantity": 2, 73 | "size": 36 74 | } 75 | ], 76 | "customer": [{ 77 | "name": "Sweater Polo Ralph Lauren pink", 78 | "id": 4, 79 | "sku": "M0E20000000E2X0", 80 | "quantity": 3, 81 | "size": "M" 82 | }] 83 | }, 84 | "expectedCart": [{ 85 | "name": "Sweater Polo Ralph Lauren pink", 86 | "quantity": 3, 87 | "size": "M", 88 | "color": "pink" 89 | }, { 90 | "name": "Shirt Aspesi white M", 91 | "quantity": 1, 92 | "size": 34, 93 | "color": "white" 94 | }, 95 | { 96 | "name": "Shirt Aspesi white M", 97 | "quantity": 1, 98 | "size": 36, 99 | "color": "white" 100 | }, 101 | { 102 | "name": "Shirt ”David” MU light blue", 103 | "quantity": 2, 104 | "size": 36, 105 | "color": "blue" 106 | } 107 | ] 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /third_party/storefront/tests/e2e/integration/e2e-checkout-order-summary.spec.ts: -------------------------------------------------------------------------------- 1 | import requests, { CreateCartResponse, GetShippingMethodsResponse } from '../api/requests'; 2 | import page from '../pages/factory'; 3 | 4 | context([], 'Checkout - Order Summary', () => { 5 | beforeEach(function () { 6 | cy.fixture('test-data/e2e-checkout-order-summary').then((fixture) => { 7 | this.fixtures = { 8 | data: fixture 9 | }; 10 | }); 11 | }); 12 | 13 | it('Should contain correct data in Order Summary', function () { 14 | const data = this.fixtures.data[this.test.title]; 15 | requests.createCart().then((response: CreateCartResponse) => { 16 | data.products.forEach((product) => { 17 | requests.addToCart(response.body.data.cart.id, product, product.quantity); 18 | }); 19 | requests.updateCart(response.body.data.cart.id, { addresses: { shipping: data.customer.address.shipping }}); 20 | requests.getShippingMethods(response.body.data.cart.id).then((res: GetShippingMethodsResponse) => { 21 | const shippingMethod = res.body.data.shippingMethods.find((method) => { 22 | return method.name === data.shippingMethod; 23 | }); 24 | requests.updateCart(response.body.data.cart.id, { shippingMethodId: shippingMethod.id }); 25 | }); 26 | requests.updateCart(response.body.data.cart.id, { addresses: { billing: data.customer.address.billing }}); 27 | }); 28 | page.checkout.payment.visit(); 29 | page.checkout.payment.productRow.each((row, index) => { 30 | cy.wrap(row).within(() => { 31 | page.checkout.payment.productTitleSku.within(() => { 32 | cy.contains(data.expectedCartSummary.products[index].name).should('be.visible'); 33 | }); 34 | page.checkout.payment.productTitleSku.within(() => { 35 | cy.contains(data.expectedCartSummary.products[index].sku).should('be.visible'); 36 | }); 37 | page.checkout.payment.productAttributes.within(() => { 38 | cy.contains(data.expectedCartSummary.products[index].size).should('be.visible'); 39 | }); 40 | page.checkout.payment.productAttributes.within(() => { 41 | cy.contains(data.expectedCartSummary.products[index].color).should('be.visible'); 42 | }); 43 | page.checkout.payment.productQuantity.within(() => { 44 | cy.contains(data.expectedCartSummary.products[index].quantity).should('be.visible'); 45 | }); 46 | page.checkout.payment.productPrice.within(() => { 47 | cy.contains(data.expectedCartSummary.products[index].amount).should('be.visible'); 48 | }); 49 | }); 50 | }); 51 | page.checkout.payment.discountedPrice.within(() => { 52 | cy.contains(data.expectedCartSummary.discountedPrice).should('be.visible'); 53 | }); 54 | page.checkout.payment.specialPrice.within(() => { 55 | cy.contains(data.expectedCartSummary.specialPrice).should('be.visible'); 56 | }); 57 | page.checkout.payment.totalPrice.within(() => { 58 | cy.contains(data.expectedCartSummary.totalPrice).should('be.visible'); 59 | }); 60 | }); 61 | 62 | }); 63 | -------------------------------------------------------------------------------- /third_party/storefront/components/Checkout/UserBillingAddresses.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 79 | 80 | 99 | -------------------------------------------------------------------------------- /third_party/storefront/components/AppFooter.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 70 | 71 | 107 | -------------------------------------------------------------------------------- /third_party/storefront/tests/e2e/integration/e2e-place-order.spec.ts: -------------------------------------------------------------------------------- 1 | import page from '../pages/factory'; 2 | import generator from '../utils/data-generator'; 3 | import intercept from '../utils/network'; 4 | 5 | context('Order placement', () => { 6 | beforeEach(function () { 7 | cy.fixture('test-data/e2e-place-order').then((fixture) => { 8 | this.fixtures = { 9 | data: fixture 10 | }; 11 | }); 12 | }); 13 | 14 | it(['happypath', 'regression'], 'Should successfully place an order as a guest', function() { 15 | const data = this.fixtures.data[this.test.title]; 16 | const getProductReq = intercept.getProduct(); 17 | page.home.visit(); 18 | page.home.header.categories.first().click(); 19 | page.category().products.first().click().then(() => { 20 | cy.wait([getProductReq, getProductReq]); 21 | }); 22 | page.product().addToCartButton.click(); 23 | page.product().header.openCart(); 24 | page.components.cart.goToCheckoutButton.click(); 25 | page.checkout.shipping.heading.should('be.visible'); 26 | page.checkout.shipping.fillForm(data.customer); 27 | page.checkout.shipping.selectShippingButton.click(); 28 | page.checkout.shipping.shippingMethods.first().click(); 29 | page.checkout.shipping.continueToBillingButton.click(); 30 | page.checkout.billing.heading.should('be.visible'); 31 | page.checkout.billing.copyAddressLabel.click(); 32 | page.checkout.billing.continueToPaymentButton.click(); 33 | page.checkout.payment.paymentMethods.first().click(); 34 | page.checkout.payment.terms.click(); 35 | page.checkout.payment.makeAnOrderButton.click(); 36 | page.checkout.thankyou.heading.should('be.visible'); 37 | }); 38 | 39 | it(['happypath', 'regression'], 'Should successfully place an order as a registered customer', function() { 40 | const data = this.fixtures.data[this.test.title]; 41 | const getProductReq = intercept.getProduct(); 42 | data.customer.email = generator.email; 43 | page.home.visit(); 44 | page.home.header.openLoginModal(); 45 | page.components.loginModal.fillForm(data.customer); 46 | page.components.loginModal.iWantToCreateAccountCheckbox.click(); 47 | page.components.loginModal.submitButton.click(); 48 | page.home.header.categories.first().click(); 49 | page.category().products.first().click().then(() => { 50 | cy.wait([getProductReq, getProductReq]); 51 | }); 52 | page.product().addToCartButton.click(); 53 | page.product().header.openCart(); 54 | page.components.cart.goToCheckoutButton.click(); 55 | page.checkout.shipping.heading.should('be.visible'); 56 | page.checkout.shipping.addresses.first().click(); 57 | page.checkout.shipping.selectShippingButton.click(); 58 | page.checkout.shipping.shippingMethods.first().click(); 59 | page.checkout.shipping.continueToBillingButton.click(); 60 | page.checkout.billing.heading.should('be.visible'); 61 | page.checkout.billing.copyAddressLabel.click(); 62 | page.checkout.billing.continueToPaymentButton.click(); 63 | page.checkout.payment.paymentMethods.first().click(); 64 | page.checkout.payment.terms.click(); 65 | page.checkout.payment.makeAnOrderButton.click(); 66 | page.checkout.thankyou.heading.should('be.visible'); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /third_party/storefront/components/InstagramFeed.vue: -------------------------------------------------------------------------------- 1 | 27 | 50 | 102 | -------------------------------------------------------------------------------- /third_party/storefront/components/MyAccount/PasswordResetForm.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 90 | 91 | 122 | -------------------------------------------------------------------------------- /third_party/storefront/pages/MyAccount/MyProfile.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 99 | 100 | 119 | -------------------------------------------------------------------------------- /third_party/storefront/tests/e2e/integration/e2e-carts-merging.spec.ts: -------------------------------------------------------------------------------- 1 | import requests, { CreateCartResponse } from '../api/requests'; 2 | import page from '../pages/factory'; 3 | import generator from '../utils/data-generator'; 4 | 5 | context(['regression'], 'Carts merging', () => { 6 | beforeEach(function () { 7 | cy.fixture('test-data/e2e-carts-merging').then((fixture) => { 8 | this.fixtures = { 9 | data: fixture 10 | }; 11 | }); 12 | }); 13 | 14 | it('Should merge guest cart with registered customer cart', function () { 15 | const data = this.fixtures.data[this.test.title]; 16 | data.customer.email = generator.email; 17 | requests.getMe(); 18 | requests.createCart().then((response: CreateCartResponse) => { 19 | data.products.forEach(product => { 20 | requests.addToCart(response.body.data.cart.id, product, product.quantity); 21 | }); 22 | }); 23 | requests.customerSignMeUp(data.customer); 24 | page.home.visit(); 25 | page.home.header.openCart(); 26 | page.components.cart.productName.each((name, index) => { 27 | cy.wrap(name).should('contain', data.expectedCart[index].name); 28 | }); 29 | page.components.cart.quantity().each((input, index) => { 30 | cy.wrap(input).should('have.value', data.expectedCart[index].quantity); 31 | }); 32 | page.components.cart.product().each((product, index) => { 33 | page.components.cart.productSizeProperty(product).should('contain', data.expectedCart[index].size); 34 | page.components.cart.productColorProperty(product).should('contain', data.expectedCart[index].color); 35 | }); 36 | 37 | }); 38 | it('Should merge guest cart with registered customer cart - products already in cart', function () { 39 | const data = this.fixtures.data[this.test.title]; 40 | data.customer.email = generator.email; 41 | requests.customerSignMeUp(data.customer); 42 | requests.createCart().then((response: CreateCartResponse) => { 43 | data.products.customer.forEach(product => { 44 | requests.addToCart(response.body.data.cart.id, product, product.quantity); 45 | }); 46 | cy.clearCookies(); 47 | }); 48 | requests.createCart().then((response: CreateCartResponse) => { 49 | data.products.guest.forEach(product => { 50 | requests.addToCart(response.body.data.cart.id, product, product.quantity); 51 | }); 52 | }); 53 | page.home.visit(); 54 | page.home.header.openLoginModal(); 55 | page.components.loginModal.loginToAccountButton.click(); 56 | page.components.loginModal.fillForm(data.customer); 57 | page.components.loginModal.loginBtn.click(); 58 | page.home.header.openCart(); 59 | page.components.cart.totalItems.should($ti => { 60 | const totalItems: number = data.expectedCart.reduce((total, product) => { 61 | return total + product.quantity; 62 | }, 0); 63 | expect($ti.text().trim()).to.be.equal(totalItems.toString()); 64 | }); 65 | page.components.cart.productName.each((name, index) => { 66 | cy.wrap(name).should('contain', data.expectedCart[index].name); 67 | }); 68 | page.components.cart.quantity().each((input, index) => { 69 | cy.wrap(input).should('have.value', data.expectedCart[index].quantity); 70 | }); 71 | page.components.cart.product().each((product, index) => { 72 | page.components.cart.productSizeProperty(product).should('contain', data.expectedCart[index].size); 73 | page.components.cart.productColorProperty(product).should('contain', data.expectedCart[index].color); 74 | }); 75 | }); 76 | 77 | }); 78 | -------------------------------------------------------------------------------- /third_party/storefront/components/MyAccount/ProfileUpdateForm.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 91 | 92 | 124 | -------------------------------------------------------------------------------- /third_party/storefront/composables/useUiHelpers/index.ts: -------------------------------------------------------------------------------- 1 | import { getCurrentInstance } from '@vue/composition-api'; 2 | import { Category } from '@vue-storefront/commercetools-api'; 3 | import { AgnosticFacet } from '@vue-storefront/core'; 4 | 5 | const nonFilters = ['page', 'sort', 'phrase', 'itemsPerPage']; 6 | 7 | const getInstance = () => { 8 | const vm = getCurrentInstance(); 9 | return vm.$root as any; 10 | }; 11 | 12 | const reduceFilters = (query) => (prev, curr) => { 13 | const makeArray = Array.isArray(query[curr]) || nonFilters.includes(curr); 14 | 15 | return { 16 | ...prev, 17 | [curr]: makeArray ? query[curr] : [query[curr]] 18 | }; 19 | }; 20 | 21 | const getFiltersDataFromUrl = (context, onlyFilters) => { 22 | const { query } = context.$router.history.current; 23 | 24 | return Object.keys(query) 25 | .filter(f => onlyFilters ? !nonFilters.includes(f) : nonFilters.includes(f)) 26 | .reduce(reduceFilters(query), {}); 27 | }; 28 | 29 | const useUiHelpers = () => { 30 | const instance = getInstance(); 31 | 32 | const getFacetsFromURL = () => { 33 | const { query, params } = instance.$router.history.current; 34 | const categorySlug = Object.keys(params).reduce((prev, curr) => params[curr] || prev, params.slug_1); 35 | 36 | return { 37 | rootCatSlug: params.slug_1, 38 | categorySlug, 39 | page: parseInt(query.page, 10) || 1, 40 | sort: query.sort || 'latest', 41 | filters: getFiltersDataFromUrl(instance, true), 42 | itemsPerPage: parseInt(query.itemsPerPage, 10) || 20, 43 | phrase: query.phrase 44 | }; 45 | }; 46 | 47 | const getSearchTermFromUrl = () => { 48 | const { query, params } = instance.$router.history.current; 49 | // hardcoded categorySlug for search results 50 | const categorySlug = 'women-clothing-jackets'; 51 | 52 | return { 53 | rootCatSlug: params.slug_1, 54 | categorySlug, 55 | page: parseInt(query.page, 10) || 1, 56 | sort: query.sort || 'latest', 57 | filters: getFiltersDataFromUrl(instance, true), 58 | itemsPerPage: parseInt(query.itemsPerPage, 10) || 20, 59 | phrase: query.phrase 60 | }; 61 | }; 62 | 63 | const getCatLink = (category: Category): string => { 64 | return `/c/${instance.$route.params.slug_1}/${category.slug}`; 65 | }; 66 | 67 | const changeSorting = (sort: string) => { 68 | const { query } = instance.$router.history.current; 69 | instance.$router.push({ query: { ...query, sort } }); 70 | }; 71 | 72 | const changeFilters = (filters: any) => { 73 | instance.$router.push({ 74 | query: { 75 | ...getFiltersDataFromUrl(instance, false), 76 | ...filters 77 | } 78 | }); 79 | }; 80 | 81 | const changeItemsPerPage = (itemsPerPage: number) => { 82 | instance.$router.push({ 83 | query: { 84 | ...getFiltersDataFromUrl(instance, false), 85 | itemsPerPage 86 | } 87 | }); 88 | }; 89 | 90 | const setTermForUrl = (term: string) => { 91 | instance.$router.push({ 92 | query: { 93 | ...getFiltersDataFromUrl(instance, false), 94 | phrase: term || undefined 95 | } 96 | }); 97 | }; 98 | 99 | const isFacetColor = (facet: AgnosticFacet): boolean => facet.id === 'color'; 100 | 101 | const isFacetCheckbox = (): boolean => false; 102 | 103 | return { 104 | getFacetsFromURL, 105 | getCatLink, 106 | changeSorting, 107 | changeFilters, 108 | changeItemsPerPage, 109 | setTermForUrl, 110 | isFacetColor, 111 | isFacetCheckbox, 112 | getSearchTermFromUrl 113 | }; 114 | }; 115 | 116 | export default useUiHelpers; 117 | -------------------------------------------------------------------------------- /third_party/storefront/tests/e2e/integration/e2e-checkout-shipping-validation.spec.ts: -------------------------------------------------------------------------------- 1 | import requests, { CreateCartResponse } from '../api/requests'; 2 | import page from '../pages/factory'; 3 | import generator from '../utils/data-generator'; 4 | 5 | context(['regression'], 'Checkout - Shipping', () => { 6 | beforeEach(function () { 7 | cy.fixture('test-data/e2e-checkout-shipping-validation').then((fixture) => { 8 | this.fixtures = { 9 | data: fixture 10 | }; 11 | }); 12 | }); 13 | 14 | it('Should successfully save address - guest customer', function () { 15 | const data = this.fixtures.data[this.test.title]; 16 | requests.createCart().then((response: CreateCartResponse) => { 17 | requests.addToCart(response.body.data.cart.id, data.product); 18 | }); 19 | page.checkout.shipping.visit(); 20 | page.checkout.shipping.fillForm(data.customer); 21 | page.checkout.shipping.selectShippingButton.click(); 22 | page.checkout.shipping.shippingMethods.contains(data.shippingMethod).click(); 23 | page.checkout.shipping.continueToBillingButton.click(); 24 | page.checkout.billing.heading.should('be.visible'); 25 | }); 26 | 27 | it('Should successfully save address - registered customer', function () { 28 | const data = this.fixtures.data[this.test.title]; 29 | data.customer.email = generator.email; 30 | requests.customerSignMeUp(data.customer); 31 | requests.createCart().then((response: CreateCartResponse) => { 32 | requests.addToCart(response.body.data.cart.id, data.product); 33 | }); 34 | page.checkout.shipping.visit(); 35 | page.checkout.shipping.addNewAddressButton.click(); 36 | page.checkout.shipping.fillForm(data.customer); 37 | page.checkout.shipping.selectShippingButton.click(); 38 | page.checkout.shipping.shippingMethods.contains(data.shippingMethod).click(); 39 | page.checkout.shipping.continueToBillingButton.click(); 40 | page.checkout.billing.heading.should('be.visible'); 41 | }); 42 | 43 | const requiredFields = [ 44 | 'First Name', 45 | 'Last Name', 46 | 'Street Name', 47 | 'Apartment', 48 | 'City', 49 | 'Postal Code', 50 | 'Phone' 51 | ]; 52 | 53 | requiredFields.forEach(requiredField => { 54 | it(`Should display an error - ${requiredField} empty`, function () { 55 | const data = this.fixtures.data[this.test.title]; 56 | requests.createCart().then((response: CreateCartResponse) => { 57 | requests.addToCart(response.body.data.cart.id, data.product); 58 | }); 59 | page.checkout.shipping.visit(); 60 | page.checkout.shipping.fillForm(data.customer); 61 | page.checkout.shipping.selectShippingButton.click(); 62 | page.checkout.shipping[Cypress._.camelCase(requiredField)].parent().within(() => { 63 | cy.get('input').then(($input) => { 64 | expect($input[0].validationMessage).to.be.eq(data.errorMessage); 65 | }); 66 | }); 67 | }); 68 | }); 69 | 70 | const requiredSelects = [ 71 | 'Country', 72 | 'State' 73 | ]; 74 | 75 | requiredSelects.forEach(requiredSelect => { 76 | it(`Should display an error - ${requiredSelect} empty`, function () { 77 | const data = this.fixtures.data[this.test.title]; 78 | requests.createCart().then((response: CreateCartResponse) => { 79 | requests.addToCart(response.body.data.cart.id, data.product); 80 | }); 81 | page.checkout.shipping.visit(); 82 | page.checkout.shipping.fillForm(data.customer); 83 | page.checkout.shipping.selectShippingButton.click(); 84 | page.checkout.shipping[Cypress._.camelCase(requiredSelect)].parent().within(() => { 85 | cy.contains(data.errorMessage).should('be.visible'); 86 | }); 87 | }); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /third_party/storefront/components/Checkout/VsfPaymentProviderMock.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 80 | 81 | 133 | -------------------------------------------------------------------------------- /third_party/storefront/tests/e2e/api/requests.ts: -------------------------------------------------------------------------------- 1 | import { Address, Customer, Product } from '../types/types'; 2 | 3 | export type CreateCartResponse = { 4 | body: { 5 | data: { 6 | cart: { 7 | id: string; 8 | } 9 | } 10 | } 11 | } 12 | 13 | export type GetShippingMethodsResponse = { 14 | body: { 15 | data: { 16 | shippingMethods: [{ 17 | id: string; 18 | name: string; 19 | }] 20 | } 21 | } 22 | } 23 | 24 | const requests = { 25 | 26 | addToCart(cartId: string, product: Product, quantity?: number): Cypress.Chainable { 27 | const options = { 28 | url: '/api/ct/addToCart', 29 | method: 'POST', 30 | headers: { 31 | Accept: 'application/json', 32 | 'Content-Type': 'application/json' 33 | }, 34 | body: [ 35 | {id: cartId, version: 1}, 36 | {id: product.id, sku: product.sku }, 37 | quantity ?? 1, 38 | null 39 | ] 40 | }; 41 | return cy.request(options); 42 | }, 43 | 44 | createCart(): Cypress.Chainable { 45 | const options = { 46 | url: '/api/ct/createCart', 47 | method: 'POST', 48 | headers: { 49 | Accept: 'application/json', 50 | 'Content-Type': 'application/json' 51 | }, 52 | body: [{}, null] 53 | }; 54 | return cy.request(options); 55 | }, 56 | 57 | customerSignMeUp(customer: Customer): Cypress.Chainable { 58 | const options = { 59 | url: '/api/ct/customerSignMeUp', 60 | method: 'POST', 61 | headers: { 62 | Accept: 'application/json', 63 | 'Content-Type': 'application/json' 64 | }, 65 | body: [ 66 | { 67 | email: customer.email, 68 | password: customer.password, 69 | firstName: customer.firstName, 70 | lastName: customer.lastName 71 | } 72 | ] 73 | }; 74 | return cy.request(options); 75 | }, 76 | 77 | getMe(): Cypress.Chainable { 78 | const options = { 79 | url: '/api/ct/getMe', 80 | method: 'POST', 81 | headers: { 82 | Accept: 'application/json', 83 | 'Content-Type': 'application/json' 84 | }, 85 | body: [ 86 | {customer: false}, null 87 | ] 88 | }; 89 | return cy.request(options); 90 | }, 91 | 92 | getShippingMethods(cartId: string): Cypress.Chainable { 93 | const options = { 94 | url: '/api/ct/getShippingMethods', 95 | method: 'POST', 96 | headers: { 97 | Accept: 'application/json', 98 | 'Content-Type': 'application/json' 99 | }, 100 | body: [ 101 | cartId 102 | ] 103 | }; 104 | return cy.request(options); 105 | }, 106 | 107 | updateCart(cartId: string, data?: { addresses?: { shipping?: Address, billing?: Address }, shippingMethodId?: string }): Cypress.Chainable { 108 | const actions = []; 109 | 110 | if (data.addresses !== undefined) { 111 | if (data.addresses.shipping !== undefined) actions.push({ 112 | setShippingAddress: { 113 | address: { 114 | ...data.addresses.shipping 115 | } 116 | } 117 | }); 118 | 119 | if (data.addresses.billing !== undefined) actions.push({ 120 | setBillingAddress: { 121 | address: { 122 | ...data.addresses.billing 123 | } 124 | } 125 | }); 126 | } 127 | 128 | if (data.shippingMethodId !== undefined) actions.push({ 129 | setShippingMethod: { 130 | shippingMethod: { 131 | id: data.shippingMethodId 132 | } 133 | } 134 | }); 135 | 136 | const options = { 137 | url: '/api/ct/updateCart', 138 | method: 'POST', 139 | headers: { 140 | Accept: 'application/json', 141 | 'Content-Type': 'application/json' 142 | }, 143 | body: [ 144 | { 145 | id: cartId, 146 | version: 1, 147 | actions: [ 148 | ...actions 149 | ] 150 | }, 151 | null 152 | ] 153 | }; 154 | return cy.request(options); 155 | } 156 | }; 157 | 158 | export default requests; 159 | -------------------------------------------------------------------------------- /third_party/storefront/pages/MyAccount.vue: -------------------------------------------------------------------------------- 1 | 50 | 135 | 136 | 160 | -------------------------------------------------------------------------------- /third_party/storefront/nuxt.config.js: -------------------------------------------------------------------------------- 1 | import webpack from "webpack"; 2 | import { VSF_LOCALE_COOKIE } from "@vue-storefront/core"; 3 | import theme from "./themeConfig"; 4 | 5 | export default { 6 | mode: "universal", 7 | server: { 8 | port: 3000, 9 | host: "0.0.0.0" 10 | }, 11 | css: ["@/static/css/main.scss"], 12 | head: { 13 | title: "Online Boutique", 14 | meta: [ 15 | { charset: "utf-8" }, 16 | { name: "viewport", content: "width=device-width, initial-scale=1" }, 17 | { 18 | hid: "description", 19 | name: "description", 20 | content: "Online Boutique Sample Store" 21 | } 22 | ], 23 | link: [ 24 | { rel: "icon", type: "image/x-icon", href: "/favicon.ico" }, 25 | { 26 | rel: "preconnect", 27 | href: "https://fonts.gstatic.com", 28 | crossorigin: "crossorigin" 29 | }, 30 | { 31 | rel: "preload", 32 | href: 33 | "https://fonts.googleapis.com/css?family=Raleway:300,400,400i,500,600,700|Roboto:300,300i,400,400i,500,700&display=swap", 34 | as: "style" 35 | }, 36 | { 37 | rel: "stylesheet", 38 | href: 39 | "https://fonts.googleapis.com/css?family=Raleway:300,400,400i,500,600,700|Roboto:300,300i,400,400i,500,700&display=swap", 40 | media: "print", 41 | onload: "this.media='all'", 42 | once: true 43 | } 44 | ], 45 | script: [] 46 | }, 47 | loading: { color: "#fff" }, 48 | router: { 49 | middleware: ["checkout"], 50 | scrollBehavior(_to, _from, savedPosition) { 51 | if (savedPosition) { 52 | return savedPosition; 53 | } else { 54 | return { x: 0, y: 0 }; 55 | } 56 | } 57 | }, 58 | buildModules: [ 59 | // to core 60 | "@nuxt/typescript-build", 61 | "@nuxtjs/style-resources", 62 | // to core soon 63 | "@nuxtjs/pwa", 64 | [ 65 | "@vue-storefront/nuxt", 66 | { 67 | coreDevelopment: true, 68 | useRawSource: { 69 | dev: ["@vue-storefront/commercetools", "@vue-storefront/core"], 70 | prod: ["@vue-storefront/commercetools", "@vue-storefront/core"] 71 | } 72 | } 73 | ], 74 | ["@vue-storefront/nuxt-theme"], 75 | [ 76 | "@vue-storefront/commercetools/nuxt", 77 | { 78 | i18n: { useNuxtI18nConfig: true } 79 | } 80 | ] 81 | ], 82 | modules: [ 83 | "nuxt-i18n", 84 | "cookie-universal-nuxt", 85 | "vue-scrollto/nuxt", 86 | "@vue-storefront/middleware/nuxt" 87 | ], 88 | i18n: { 89 | currency: "USD", 90 | country: "US", 91 | countries: [ 92 | { name: "US", label: "United States", states: ["New York", "California"] } 93 | ], 94 | currencies: [{ name: "USD", label: "Dollar" }], 95 | locales: [{ code: "en", label: "English", file: "en.js", iso: "en" }], 96 | defaultLocale: "en", 97 | lazy: true, 98 | seo: true, 99 | langDir: "lang/", 100 | vueI18n: { 101 | fallbackLocale: "en", 102 | numberFormats: { 103 | en: { 104 | currency: { 105 | style: "currency", 106 | currency: "USD", 107 | currencyDisplay: "symbol" 108 | } 109 | } 110 | } 111 | }, 112 | detectBrowserLanguage: { 113 | cookieKey: VSF_LOCALE_COOKIE 114 | } 115 | }, 116 | styleResources: { 117 | scss: [ 118 | require.resolve("@storefront-ui/shared/styles/_helpers.scss", { 119 | paths: [process.cwd()] 120 | }) 121 | ] 122 | }, 123 | publicRuntimeConfig: { 124 | theme 125 | }, 126 | build: { 127 | babel: { 128 | plugins: [["@babel/plugin-proposal-private-methods", { loose: true }]] 129 | }, 130 | transpile: ["vee-validate/dist/rules"], 131 | plugins: [ 132 | new webpack.DefinePlugin({ 133 | "process.VERSION": JSON.stringify({ 134 | // eslint-disable-next-line global-require 135 | version: require("./package.json").version, 136 | lastCommit: process.env.LAST_COMMIT || "" 137 | }) 138 | }) 139 | ], 140 | extend(config, ctx) { 141 | if (ctx && ctx.isClient) { 142 | config.optimization = { 143 | ...config.optimization, 144 | mergeDuplicateChunks: true, 145 | splitChunks: { 146 | ...config.optimization.splitChunks, 147 | chunks: "all", 148 | automaticNameDelimiter: ".", 149 | maxSize: 128_000, 150 | maxInitialRequests: Number.POSITIVE_INFINITY, 151 | minSize: 0, 152 | maxAsyncRequests: 10, 153 | cacheGroups: { 154 | vendor: { 155 | test: /[/\\]node_modules[/\\]/, 156 | name: module => 157 | `${module.context 158 | .match(/[/\\]node_modules[/\\](.*?)([/\\]|$)/)[1] 159 | .replace(/[.@_]/gm, "")}` 160 | } 161 | } 162 | } 163 | }; 164 | } 165 | } 166 | } 167 | }; 168 | -------------------------------------------------------------------------------- /third_party/storefront/tests/e2e/pages/checkout.ts: -------------------------------------------------------------------------------- 1 | import { Customer, Address } from '../types/types'; 2 | import Base from './base'; 3 | import { el } from './utils/element'; 4 | 5 | class Checkout extends Base { 6 | 7 | protected step = '' 8 | 9 | get addNewAddressButton(): Cypress.Chainable { 10 | return el(`${this.step}-add-new-address`); 11 | } 12 | 13 | get firstName(): Cypress.Chainable { 14 | return el(`${this.step}-firstName`, 'input'); 15 | } 16 | 17 | get lastName(): Cypress.Chainable { 18 | return el(`${this.step}-lastName`, 'input'); 19 | } 20 | 21 | get streetName(): Cypress.Chainable { 22 | return el(`${this.step}-streetName`, 'input'); 23 | } 24 | 25 | get apartment(): Cypress.Chainable { 26 | return el(`${this.step}-apartment`, 'input'); 27 | } 28 | 29 | get city(): Cypress.Chainable { 30 | return el(`${this.step}-city`, 'input'); 31 | } 32 | 33 | get state(): Cypress.Chainable { 34 | return el(`${this.step}-state`, 'select'); 35 | } 36 | 37 | get country(): Cypress.Chainable { 38 | return el(`${this.step}-country`, 'select'); 39 | } 40 | 41 | get postalCode(): Cypress.Chainable { 42 | return el(`${this.step}-zipcode`, 'input'); 43 | } 44 | 45 | get phone(): Cypress.Chainable { 46 | return el(`${this.step}-phone`, 'input'); 47 | } 48 | 49 | public fillForm(address: Address) { 50 | if (address.firstName !== undefined) this.firstName.clear().type(address.firstName); 51 | if (address.lastName !== undefined) this.lastName.clear().type(address.lastName); 52 | if (address.streetName !== undefined) this.streetName.clear().type(address.streetName); 53 | if (address.apartment !== undefined) { 54 | this.apartment.parent().click(); 55 | this.apartment.clear().type(address.apartment); 56 | } 57 | if (address.city !== undefined) this.city.clear().type(address.city); 58 | if (address.country !== undefined) this.country.select(address.country); 59 | if (address.state !== undefined) this.state.select(address.state); 60 | if (address.postalCode !== undefined) this.postalCode.clear().type(address.postalCode); 61 | if (address.phone !== undefined) this.phone.clear().type(address.phone); 62 | } 63 | } 64 | 65 | class Shipping extends Checkout { 66 | 67 | constructor() { 68 | super(); 69 | this.step = 'shipping'; 70 | } 71 | 72 | get path(): string { 73 | return '/checkout/shipping'; 74 | } 75 | 76 | get addresses(): Cypress.Chainable { 77 | return el('shipping-addresses', '.sf-radio label'); 78 | } 79 | 80 | get continueToBillingButton(): Cypress.Chainable { 81 | return el('continue-to-billing'); 82 | } 83 | 84 | get heading(): Cypress.Chainable { 85 | return el(`${this.step}-heading`); 86 | } 87 | 88 | get selectShippingButton(): Cypress.Chainable { 89 | return el('select-shipping'); 90 | } 91 | 92 | get shippingMethods(): Cypress.Chainable { 93 | return el('shipping-method-label'); 94 | } 95 | 96 | public fillForm(customer: Customer) { 97 | super.fillForm(customer.address.shipping); 98 | } 99 | 100 | } 101 | 102 | class Billing extends Checkout { 103 | 104 | constructor() { 105 | super(); 106 | this.step = 'billing'; 107 | } 108 | 109 | get path(): string { 110 | return '/checkout/billing'; 111 | } 112 | 113 | get continueToPaymentButton(): Cypress.Chainable { 114 | return el('continue-to-payment'); 115 | } 116 | 117 | get heading(): Cypress.Chainable { 118 | return el(`${this.step}-heading`); 119 | } 120 | 121 | get copyAddressLabel(): Cypress.Chainable { 122 | return el('copy-address', 'label'); 123 | } 124 | 125 | public fillForm(customer: Customer) { 126 | super.fillForm(customer.address.billing); 127 | } 128 | } 129 | 130 | class Payment extends Base { 131 | 132 | get path(): string { 133 | return '/checkout/payment'; 134 | } 135 | 136 | get heading(): Cypress.Chainable { 137 | return el('heading-payment'); 138 | } 139 | 140 | get makeAnOrderButton(): Cypress.Chainable { 141 | return el('make-an-order'); 142 | } 143 | 144 | get paymentMethods(): Cypress.Chainable { 145 | return el('payment-method'); 146 | } 147 | 148 | get terms(): Cypress.Chainable { 149 | return el('terms', 'label'); 150 | } 151 | 152 | get productRow(): Cypress.Chainable { 153 | return el('product-row'); 154 | } 155 | 156 | get productTitleSku(): Cypress.Chainable { 157 | return el('product-title-sku'); 158 | } 159 | 160 | get productAttributes(): Cypress.Chainable { 161 | return el('product-attributes'); 162 | } 163 | 164 | get productQuantity(): Cypress.Chainable { 165 | return el('product-quantity'); 166 | } 167 | 168 | get productPrice(): Cypress.Chainable { 169 | return el('product-price'); 170 | } 171 | 172 | get discountedPrice(): Cypress.Chainable { 173 | return cy.get('.discounted'); 174 | } 175 | 176 | get specialPrice(): Cypress.Chainable { 177 | return cy.get('.special-price'); 178 | } 179 | 180 | get totalPrice(): Cypress.Chainable { 181 | return cy.get('.property-total'); 182 | } 183 | } 184 | 185 | class ThankYou { 186 | get heading(): Cypress.Chainable { 187 | return el('thank-you-banner', 'h2'); 188 | } 189 | } 190 | 191 | export { 192 | Shipping, 193 | Billing, 194 | Payment, 195 | ThankYou 196 | }; 197 | -------------------------------------------------------------------------------- /third_party/storefront/pages/ResetPassword.vue: -------------------------------------------------------------------------------- 1 | 65 | 124 | 125 | 168 | -------------------------------------------------------------------------------- /third_party/storefront/components/StoreLocaleSelector.vue: -------------------------------------------------------------------------------- 1 | 55 | 56 | 124 | 125 | 187 | -------------------------------------------------------------------------------- /third_party/storefront/tests/e2e/integration/e2e-checkout-billing-validation.spec.ts: -------------------------------------------------------------------------------- 1 | import requests, { CreateCartResponse } from '../api/requests'; 2 | import page from '../pages/factory'; 3 | import generator from '../utils/data-generator'; 4 | 5 | context(['regression'], 'Checkout - Billing', () => { 6 | beforeEach(function () { 7 | cy.fixture('test-data/e2e-checkout-billing-validation').then((fixture) => { 8 | this.fixtures = { 9 | data: fixture 10 | }; 11 | }); 12 | }); 13 | 14 | it('Should successfully save address - guest customer', function () { 15 | const data = this.fixtures.data[this.test.title]; 16 | requests.createCart().then((response: CreateCartResponse) => { 17 | requests.addToCart(response.body.data.cart.id, data.product); 18 | requests.updateCart(response.body.data.cart.id, { addresses: { shipping: data.customer.address.shipping }}); 19 | }); 20 | page.checkout.shipping.visit(); 21 | page.checkout.shipping.selectShippingButton.click(); 22 | page.checkout.shipping.shippingMethods.contains(data.shippingMethod).click(); 23 | page.checkout.shipping.continueToBillingButton.click(); 24 | page.checkout.billing.heading.should('be.visible'); 25 | page.checkout.billing.fillForm(data.customer); 26 | page.checkout.billing.continueToPaymentButton.click(); 27 | page.checkout.payment.heading.should('be.visible'); 28 | }); 29 | 30 | it('Should successfully save address - registered customer', function () { 31 | const data = this.fixtures.data[this.test.title]; 32 | data.customer.email = generator.email; 33 | requests.customerSignMeUp(data.customer); 34 | requests.createCart().then((response: CreateCartResponse) => { 35 | requests.addToCart(response.body.data.cart.id, data.product); 36 | requests.updateCart(response.body.data.cart.id, { addresses: { shipping: data.customer.address.shipping }}); 37 | }); 38 | page.checkout.shipping.visit(); 39 | page.checkout.shipping.selectShippingButton.click(); 40 | page.checkout.shipping.shippingMethods.contains(data.shippingMethod).click(); 41 | page.checkout.shipping.continueToBillingButton.click(); 42 | page.checkout.billing.heading.should('be.visible'); 43 | page.checkout.billing.addNewAddressButton.click(); 44 | page.checkout.billing.fillForm(data.customer); 45 | page.checkout.billing.continueToPaymentButton.click(); 46 | page.checkout.payment.heading.should('be.visible'); 47 | }); 48 | 49 | const requiredFields = [ 50 | 'First Name', 51 | 'Last Name', 52 | 'Street Name', 53 | 'Apartment', 54 | 'City', 55 | 'Postal Code', 56 | 'Phone' 57 | ]; 58 | 59 | requiredFields.forEach(requiredField => { 60 | it(`Should display an error - ${requiredField} empty`, function () { 61 | const data = this.fixtures.data[this.test.title]; 62 | requests.createCart().then((response: CreateCartResponse) => { 63 | requests.addToCart(response.body.data.cart.id, data.product); 64 | requests.updateCart(response.body.data.cart.id, { addresses: { shipping: data.customer.address.shipping }}); 65 | }); 66 | page.checkout.shipping.visit(); 67 | page.checkout.shipping.selectShippingButton.click(); 68 | page.checkout.shipping.shippingMethods.contains(data.shippingMethod).click(); 69 | page.checkout.shipping.continueToBillingButton.click(); 70 | page.checkout.billing.heading.should('be.visible'); 71 | page.checkout.billing.fillForm(data.customer); 72 | page.checkout.billing.continueToPaymentButton.click(); 73 | page.checkout.billing[Cypress._.camelCase(requiredField)].parent().within(() => { 74 | cy.get('input').then(($input) => { 75 | expect($input[0].validationMessage).to.be.eq(data.errorMessage); 76 | }); 77 | }); 78 | }); 79 | }); 80 | 81 | const requiredSelects = [ 82 | 'Country', 83 | 'State' 84 | ]; 85 | 86 | requiredSelects.forEach(requiredSelect => { 87 | it(`Should display an error - ${requiredSelect} empty`, function () { 88 | const data = this.fixtures.data[this.test.title]; 89 | requests.createCart().then((response: CreateCartResponse) => { 90 | requests.addToCart(response.body.data.cart.id, data.product); 91 | requests.updateCart(response.body.data.cart.id, { addresses: { shipping: data.customer.address.shipping }}); 92 | }); 93 | page.checkout.shipping.visit(); 94 | page.checkout.shipping.selectShippingButton.click(); 95 | page.checkout.shipping.shippingMethods.contains(data.shippingMethod).click(); 96 | page.checkout.shipping.continueToBillingButton.click(); 97 | page.checkout.billing.heading.should('be.visible'); 98 | page.checkout.billing.fillForm(data.customer); 99 | page.checkout.billing.continueToPaymentButton.click(); 100 | page.checkout.billing[Cypress._.camelCase(requiredSelect)].parent().within(() => { 101 | cy.contains(data.errorMessage).should('be.visible'); 102 | }); 103 | }); 104 | }); 105 | 106 | it('Should copy shipping address', function () { 107 | const data = this.fixtures.data[this.test.title]; 108 | requests.createCart().then((response: CreateCartResponse) => { 109 | requests.addToCart(response.body.data.cart.id, data.product); 110 | requests.updateCart(response.body.data.cart.id, { addresses: { shipping: data.customer.address.shipping }}); 111 | }); 112 | page.checkout.shipping.visit(); 113 | page.checkout.shipping.selectShippingButton.click(); 114 | page.checkout.shipping.shippingMethods.contains(data.shippingMethod).click(); 115 | page.checkout.shipping.continueToBillingButton.click(); 116 | page.checkout.billing.heading.should('be.visible'); 117 | page.checkout.billing.copyAddressLabel.click(); 118 | for (const field in data.customer.address.shipping) { 119 | console.log(field); 120 | page.checkout.billing[field].should('have.value', data.customer.address.shipping[field]); 121 | } 122 | }); 123 | }); 124 | -------------------------------------------------------------------------------- /third_party/storefront/pages/MyAccount/BillingDetails.vue: -------------------------------------------------------------------------------- 1 | 72 | 137 | 138 | 215 | -------------------------------------------------------------------------------- /third_party/storefront/pages/MyAccount/ShippingDetails.vue: -------------------------------------------------------------------------------- 1 | 72 | 137 | 138 | 214 | --------------------------------------------------------------------------------