[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of product-highlights/docs/README.md from the product-highlights VTEX app.

[Assistant]
Here's the implementation:

```markdown
📢 Use this project, [contribute](https://github.com/vtex-apps/product-highlights) to it or open issues to help evolve it using [Store Discussion](https://github.com/vtex-apps/store-discussion).

# Product Highlights

<!-- DOCS-IGNORE:start -->
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
[![All Contributors](https://img.shields.io/badge/all_contributors-1-orange.svg?style=flat-square)](#contributors-)
<!-- ALL-CONTRIBUTORS-BADGE:END -->
<!-- DOCS-IGNORE:end -->

The Product Highlights app provides blocks to display highlight badges on products according to the collection or promotion they are linked to.

![Product Highlights Example](https://cdn.jsdelivr.net/gh/vtexdocs/dev-portal-content@main/images/vtex-product-highlights-0.png)

_In the image above, the product has a `Top Seller` highlight._

## Configuration

### Step 1 - Adding the Product Highlights app to your theme dependencies

In your theme `manifest.json` file, add the Product Highlights app as a dependency:

```diff
 "dependencies": {
+  "vtex.product-highlights": "2.x"
}
```

Now, you can use all the blocks exported by the `product-highlights` app. See the full list below:

| Block name                  | Description                                                                                |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `product-highlights`        | Parent block for defining how the Product Highlights component will be displayed, based on its children blocks (`product-highlight-text` and `product-highlight-wrapper`) and props. |
| `product-highlight-text`    | Renders a `span` HTML tag with the highlight name. It also provides data attributes and CSS handles for style customizations.                                                        |
| `product-highlight-wrapper` | You may use this block if you need to render other blocks alongside the highlight name. It renders a `div` HTML tag and its children blocks, if any.                                 |

### Step 2 - Adding the Product Highlights blocks to your theme templates

Copy one of the examples stated below and paste it into your desired theme template, making the necessary changes. If needed, add the `product-highlights` block to the template block list.

- Simple example:

```json
{
  "vtex.product-highlights@2.x:product-highlights": {
    "children": ["product-highlight-text"]
  },
  "product-highlight-text": {
    "props": {
      "message": "{highlightName}"
    }
  }
}
```

- Example using the `link` prop:

```json
{
  "vtex.product-highlights@2.x:product-highlights": {
    "children": ["product-highlight-text"]
  },
  "product-highlight-text": {
    "props": {
      "message": "{highlightName}",
      "link": "/collection/{highlightId}"
    }
  }
}
```

- Example using `product-highlight-wrapper`:

```jsonc
{
  "vtex.product-highlights@2.x:product-highlights": {
    "children": ["product-highlight-wrapper"]
  },
  "product-highlight-wrapper": {
    "children": [
      "icon-star", // You can add anything inside a product-highlight-wrapper
      "product-highlight-text"
    ]
  },
  "product-highlight-text": {
    "props": {
      "message": "{highlightName}"
    }
  }
}
```

- Example using the prop `filter` and the prop `type`:

```jsonc
{
  "vtex.product-highlights@2.x:product-highlights": {
    "props": {
      "type": "teaser",
      "filter": {
        "type": "show",
        "highlightNames": ["10% Boleto"]
      }
    },
    "children": ["product-highlight-text"]
  },
  "product-highlight-text": {
    "props": {
      "message": "{highlightName}",
      "blockClass": "boleto"
    }
  }
}
```

> ⚠ warning
>
> Note that **the Product Highlights blocks require a Product context to work properly, as they handle product data**. Therefore, when declaring these blocks, ensure that they are in a theme template or block where this context is available, such as the `store.product` and `product-summary.shelf`.

#### `product-highlights` props

| Prop name | Type     | Description                                                                                | Default value |
| --------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
| `type`    | `enum`   | The desired type of product highlight to be displayed. Possible values are: `collection`, `promotion`, and `teaser`. `collection` highlights the product's collection, so it must be used with the [Collection Highlight](https://help.vtex.com/en/tutorial/collection-highlight-control--1tGdb2ndjqy6yWsk2YwKMu?locale=en) feature. `promotion` and `teaser` should be used when the product is configured with a [promotion that includes highlights](https://help.vtex.com/en/tutorial/configuring-promotions-with-a-highlightflag--tutorials_2295?locale=en). It can be used even if the promotion does not have any restrictions. On the other hand, the `teaser` must only be used when the promotion has restrictions.   ⚠* Note that nominal promotions will only be displayed in the cart, not on the shelf or product page.* | `collection`  |
| `filter`  | `object` | Defines which highlights should and should not be displayed by the block.                                                                                | `undefined`   |

>⚠ Technically, the `collection` highlight maps to the [`clusterHighlights`](https://github.com/vtex-apps/search-graphql/blob/ea1d7e244e6b00b58e5aa4272fbb16987c483468/graphql/types/Product.graphql#L29)  property; the `promotion` highlight maps to the [`discountHighlights`](https://github.com/vtex-apps/search-graphql/blob/ea1d7e244e6b00b58e5aa4272fbb16987c483468/graphql/types/Product.graphql#L283) property; and the `teaser` highlight maps to the [`teasers`](https://github.com/vtex-apps/search-graphql/blob/ea1d7e244e6b00b58e5aa4272fbb16987c483468/graphql/types/Product.graphql#L284) property.

- **`filter` object:**

| Prop name        | Type       | Description                                                                                | Default value |
| ---------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
| `highlightNames` | `[string]` | An array of highlight names that determine which highlights should be hidden or shown based on the `type` property defined in the `product-highlights` block.                                                                                | `undefined`   |
| `type`           | `enum`     | Determines whether the highlights for the `highlightNames` prop should be displayed or hidden on the UI. Possible values are: `hide` (hides highlights declared in the `highlightNames` prop) or `show` (only shows the highlights declared in the `highlightNames` prop). | `undefined`   |

#### `product-highlight-text` props

| Prop name    | Type       | Description                                                                                | Default value |
| ------------ | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
| `blockClass` | `string`   | The block ID you chose to be used for [CSS customization](https://developers.vtex.com/docs/guides/vtex-io-documentation-using-css-handles-for-store-customization#using-the-blockclass-property).                                                                                | `undefined`   |
| `message`    | `string`   | Defines the block default text message to be rendered in the UI. You can also use the Admin Site Editor and the `markers` prop to specify the text message that the block will render on the UI.                                                                                | `undefined`   |
| `markers`    | `[string]` | IDs you chose to identify the block's rendered text message and customize it using the Admin Site Editor. Learn how to use them in [Using the Markers prop to customize a block message](https://developers.vtex.com/docs/guides/vtex-io-documentation-using-the-markers-prop-to-customize-a-blocks-message). Note that a block message can also be customized in the Store Theme source code using the `message` prop. | `[]`          |
| `link`       | `string`   | If this prop is set, it creates a link to the string passed. You can interpolate the variables `highlightText` and `highlightId`. Example: `/collection/{highlightId}`.                                                                                | `undefined`   |

#### `product-highlight-wrapper` props

| Prop name    | Type     | Description                                                                                | Default value |
| ------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------- |
| `blockClass` | `string` | The block ID you chose to be used in [CSS customization](https://developers.vtex.com/docs/guides/vtex-io-documentation-using-css-handles-for-store-customization#using-the-blockclass-property). | `undefined`   |

### Step 3 - Editing the `product-highlight-text` messages

The `product-highlight-text` block uses the [ICU Message Format](https://format-message.github.io/icu-message-format-for-translators/), enabling comprehensive customization of the rendered text messages.

When using the `message` prop, you will not need to create an advanced configuration: declare the prop directly in your Store Theme app and pass to it the desired text value to be rendered with the block.

The `markers` prop, in turn, requires you to add an extra configuration in the Site Editor of the Admin to properly work. When using this prop, do not forget to check out the block's message variables (shown in the table below) and the [Using the Markers prop to customize a block's message](https://developers.vtex.com/docs/guides/vtex-io-documentation-using-the-markers-prop-to-customize-a-blocks-message) documentation.

| Message variable | Type     | Description          |
| ---------------- | -------- | -------------------- |
| `highlightName`  | `string` | Highlights the name. |

## Customization

To apply CSS customizations to this and other blocks, follow the instructions in [Using CSS handles for store customizations](https://developers.vtex.com/docs/guides/vtex-io-documentation-using-css-handles-for-store-customization).

| CSS handles               |
| ------------------------- |
| `productHighlightText`    |
| `productHighlightWrapper` |

| Data attributes       |
| --------------------- |
| `data-highlight-name` |
| `data-highlight-id`   |
| `data-highlight-type` |

<!-- DOCS-IGNORE:start -->

## Contributors ✨

Thanks goes to these wonderful people:

<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
  <tr>
    <td align="center"><a href="https://www.linkedin.com/in/igor-becker-73b7901a3/"><img src="https://avatars.githubusercontent.com/u/57180581?v=4?s=100" width="100px;" alt=""/><br /><sub><b>proudynyu</b></sub></a><br /><a href="https://github.com/vtex-apps/product-highlights/commits?author=proudynyu" title="Code">💻</a></td>
  </tr>
</table>

<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->

<!-- ALL-CONTRIBUTORS-LIST:END -->

This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind are welcome!

<!-- DOCS-IGNORE:end -->

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of product-highlights/react/ProductHighlights.test.tsx from the product-highlights VTEX app.

[Assistant]
Here's the implementation:

```tsx
import React from 'react'
import { render } from '@vtex/test-tools/react'
import type { ProductContext } from 'vtex.product-context'
import { useProduct } from 'vtex.product-context'

import ProductHighlights from './ProductHighlights'
import ProductHighlightText from './ProductHighlightText'
import tankTop from './__fixtures__/tankTop'

const mockUseProduct = useProduct as jest.Mock<ProductContext>

mockUseProduct.mockImplementation(() => ({ product: tankTop }))

test("render all collections' highlights", () => {
  const { queryByText, rerender } = render(
    <ProductHighlights>
      <ProductHighlightText message="{highlightName}" />
    </ProductHighlights>
  )

  expect(queryByText('Top Sellers')).toBeInTheDocument()
  expect(queryByText('Summer')).toBeInTheDocument()

  rerender(
    <ProductHighlights type="collection">
      <ProductHighlightText message="{highlightName}" />
    </ProductHighlights>
  )

  expect(queryByText('Top Sellers')).toBeInTheDocument()
  expect(queryByText('Summer')).toBeInTheDocument()
})

test("render all promotions' highlights", () => {
  const { queryByText } = render(
    <ProductHighlights type="promotion">
      <ProductHighlightText message="{highlightName}" />
    </ProductHighlights>
  )

  expect(queryByText('Free Shipping')).toBeInTheDocument()
  expect(queryByText('10% Discount')).toBeInTheDocument()
})

test("render all teasers' highlights", () => {
  const { queryByText } = render(
    <ProductHighlights type="teaser">
      <ProductHighlightText message="{highlightName}" />
    </ProductHighlights>
  )

  expect(queryByText('8% Boleto')).toBeInTheDocument()
  expect(queryByText('5% Visa')).toBeInTheDocument()
})

test('render nothing if wrong type is passed', () => {
  const { container } = render(
    // eslint-disable-next-line @typescript-eslint/ban-ts-ignore
    // @ts-ignore
    <ProductHighlights type="">
      <ProductHighlightText message="{highlightName}" />
    </ProductHighlights>
  )

  expect(container).toBeEmptyDOMElement()
})

test("render nothing if there's no product context", () => {
  mockUseProduct.mockImplementationOnce(() => ({ product: undefined }))

  const { container } = render(
    <ProductHighlights>
      <ProductHighlightText message="{highlightName}" />
    </ProductHighlights>
  )

  expect(container).toBeEmptyDOMElement()
})

test('should filter highlights', () => {
  const { queryByText, rerender } = render(
    <ProductHighlights filter={{ type: 'hide', highlightNames: ['Summer'] }}>
      <ProductHighlightText message="{highlightName}" />
    </ProductHighlights>
  )

  expect(queryByText('Top Sellers')).toBeInTheDocument()
  expect(queryByText('Summer')).not.toBeInTheDocument()

  rerender(
    <ProductHighlights filter={{ type: 'show', highlightNames: ['Summer'] }}>
      <ProductHighlightText message="{highlightName}" />
    </ProductHighlights>
  )

  expect(queryByText('Top Sellers')).not.toBeInTheDocument()
  expect(queryByText('Summer')).toBeInTheDocument()

  rerender(
    <ProductHighlights
      filter={{
        type: 'hide',
        highlightNames: ['Summer', 'Top Sellers'],
      }}
    >
      <ProductHighlightText message="{highlightName}" />
    </ProductHighlights>
  )

  expect(queryByText('Top Sellers')).not.toBeInTheDocument()
  expect(queryByText('Summer')).not.toBeInTheDocument()

  rerender(
    <ProductHighlights
      filter={{
        type: 'show',
        highlightNames: ['Summer', 'Top Sellers'],
      }}
    >
      <ProductHighlightText message="{highlightName}" />
    </ProductHighlights>
  )

  expect(queryByText('Top Sellers')).toBeInTheDocument()
  expect(queryByText('Summer')).toBeInTheDocument()
})

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of product-highlights/react/ProductHighlights.tsx from the product-highlights VTEX app.

[Assistant]
Here's the implementation:

```tsx
import type { FC, ReactNode } from 'react'
import React, { useContext, useMemo } from 'react'
import { defineMessages } from 'react-intl'
import type { ProductTypes } from 'vtex.product-context'
import { useProduct } from 'vtex.product-context'

import { getSeller } from './modules/seller'

type HighlightType = 'collection' | 'promotion' | 'teaser'

interface Filter {
  type: 'hide' | 'show'
  highlightNames: string[]
}

interface ProductHighlightsProps {
  filter?: Filter
  type?: HighlightType
  children: NonNullable<ReactNode>
}

const defaultFilter: Filter = {
  type: 'hide',
  highlightNames: [],
}

interface Highlight {
  id?: string
  name: string
}

interface ProductHighlightContextProviderProps {
  highlight: Highlight
  type: HighlightType
}

function createFilterHighlight(filter: Filter) {
  return function filterHighlight(highlight: Highlight) {
    const hasHighlight = filter.highlightNames?.includes(highlight.name)

    if (
      (filter.type === 'hide' && hasHighlight) ||
      (filter.type === 'show' && !hasHighlight)
    ) {
      return false
    }

    return true
  }
}

const ProductHighlightContext =
  React.createContext<ProductHighlightContextProviderProps | null>(null)

const ProductHighlightContextProvider: FC<ProductHighlightContextProviderProps> =
  ({ highlight, type, children }) => {
    const contextValue = useMemo(
      () => ({
        highlight,
        type,
      }),
      [highlight, type]
    )

    return (
      <ProductHighlightContext.Provider value={contextValue}>
        {children}
      </ProductHighlightContext.Provider>
    )
  }

function ProductHighlights({
  filter = defaultFilter,
  type = 'collection',
  children,
}: ProductHighlightsProps) {
  const { product, selectedItem } = useProduct() ?? {}
  const selectedSku = selectedItem ?? product?.items?.[0]
  const seller: ProductTypes.Seller | null = selectedSku
    ? getSeller(selectedSku)
    : null

  const highlights = useMemo(() => {
    const clusterHighlights = product?.clusterHighlights ?? []
    const discountHighlights = seller?.commertialOffer?.discountHighlights ?? []
    const teasers = seller?.commertialOffer?.teasers ?? []

    const filterHighlight = createFilterHighlight(filter)

    if (type === 'collection') {
      return clusterHighlights.filter(filterHighlight)
    }

    if (type === 'promotion') {
      return discountHighlights.filter(filterHighlight)
    }

    if (type === 'teaser') {
      return teasers.filter(filterHighlight)
    }

    return []
  }, [
    filter,
    product?.clusterHighlights,
    seller?.commertialOffer?.discountHighlights,
    seller?.commertialOffer?.teasers,
    type,
  ])

  if (!product) {
    return null
  }

  return (
    <>
      {highlights.map((highlight, index) => (
        <ProductHighlightContextProvider
          key={index}
          type={type}
          highlight={highlight}
        >
          {children}
        </ProductHighlightContextProvider>
      ))}
    </>
  )
}

const messages = defineMessages({
  Filter: {
    defaultMessage: '',
    id: 'admin/editor.product-highlights.filter',
  },
  QueryType: {
    defaultMessage: '',
    id: 'admin/editor.product-highlights.type',
  },
  Collection: {
    defaultMessage: '',
    id: 'admin/editor.product-highlights.type.collection',
  },
  Promotion: {
    defaultMessage: '',
    id: 'admin/editor.product-highlights.type.promotion',
  },
  Teaser: {
    defaultMessage: '',
    id: 'admin/editor.product-highlights.type.teaser',
  },
  Type: {
    defaultMessage: '',
    id: 'admin/editor.product-highlights.filter.type',
  },
  Hide: {
    defaultMessage: '',
    id: 'admin/editor.product-highlights.filter.type.hide',
  },
  Show: {
    defaultMessage: '',
    id: 'admin/editor.product-highlights.filter.type.show',
  },
  HighlightNames: {
    defaultMessage: '',
    id: 'admin/editor.product-highlights.filter.highlight-names',
  },
  HighlightName: {
    defaultMessage: '',
    id: 'admin/editor.product-highlights.filter.highlight-names.item',
  },
})

ProductHighlights.schema = {
  title: 'Product Highlights',
  type: 'object',
  properties: {
    filter: {
      title: messages.Filter.id,
      type: 'object',
      properties: {
        type: {
          title: messages.Type.id,
          type: 'string',
          enum: ['hide', 'show'],
          enumNames: [messages.Hide.id, messages.Show.id],
        },
        highlightNames: {
          title: messages.HighlightNames.id,
          type: 'array',
          minItems: 0,
          maxItems: 5,
          items: {
            title: messages.HighlightName.id,
            type: 'string',
            default: '',
          },
        },
      },
    },
    type: {
      title: 'admin/editor.product-highlights.type',
      type: 'string',
      enum: ['collection', 'promotion', 'teaser'],
      enumNames: [
        messages.Collection.id,
        messages.Promotion.id,
        messages.Teaser.id,
      ],
    },
  },
}

export const useHighlight = () => {
  const group = useContext(ProductHighlightContext)

  return group
}

export default ProductHighlights

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of product-highlights/react/package.json from the product-highlights VTEX app.

[Assistant]
Here's the implementation:

```json
{
  "name": "product-highlights",
  "version": "2.3.2",
  "scripts": {
    "test": "vtex-test-tools test"
  },
  "dependencies": {
    "apollo-client": "^2.6.8",
    "react": "^16.12.0",
    "react-apollo": "^3.1.3",
    "react-dom": "^16.12.0",
    "react-intl": "^3.12.0"
  },
  "devDependencies": {
    "@apollo/react-testing": "^3.1.3",
    "@types/jest": "^25.1.4",
    "@types/node": "^13.9.8",
    "@types/react": "^16.9.31",
    "@vtex/test-tools": "^3.3.1",
    "@vtex/tsconfig": "^0.4.4",
    "apollo-cache-inmemory": "^1.6.5",
    "graphql": "^14.6.0",
    "typescript": "3.9.7",
    "vtex.css-handles": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.css-handles@0.4.4/public/@types/vtex.css-handles",
    "vtex.native-types": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.native-types@0.7.5/public/@types/vtex.native-types",
    "vtex.product-context": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.product-context@0.9.7/public/@types/vtex.product-context",
    "vtex.render-runtime": "http://vtex.vtexassets.com/_v/public/typings/v1/vtex.render-runtime@8.126.11/public/@types/vtex.render-runtime"
  }
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of product-highlights/react/ProductHighlightText.test.tsx from the product-highlights VTEX app.

[Assistant]
Here's the implementation:

```tsx
import React from 'react'
import { render } from '@vtex/test-tools/react'
import { useProduct } from 'vtex.product-context'

import ProductHighlights from './ProductHighlights'
import ProductHighlightText from './ProductHighlightText'
import tankTop from './__fixtures__/tankTop'

const mockUseProduct = useProduct as jest.Mock

mockUseProduct.mockImplementation(() => ({ product: tankTop }))

test('ProductHighlightText should render data-attributes and CSS handles', () => {
  const { getByText } = render(
    <ProductHighlights>
      <ProductHighlightText message="{highlightName}" />
    </ProductHighlights>
  )

  const topSellers = getByText('Top Sellers')
  const summer = getByText('Summer')

  expect(topSellers).toHaveAttribute('data-highlight-id', '1182')
  expect(topSellers).toHaveAttribute('data-highlight-name', 'Top Sellers')
  expect(topSellers).toHaveAttribute('data-highlight-type', 'collection')
  expect(summer).toHaveClass('productHighlightText')

  expect(summer).toHaveAttribute('data-highlight-id', '1183')
  expect(summer).toHaveAttribute('data-highlight-name', 'Summer')
  expect(summer).toHaveAttribute('data-highlight-type', 'collection')
  expect(summer).toHaveClass('productHighlightText')
})

test('ProductHighlightText should render link if prop is passed', () => {
  const { getByText } = render(
    <ProductHighlights>
      <ProductHighlightText
        message="{highlightName}"
        link="/collection/{highlightName}/{highlightId}"
      />
    </ProductHighlights>
  )

  const topSellers = getByText('Top Sellers')
  const summer = getByText('Summer')

  expect(topSellers).toHaveAttribute('href', '/collection/Top Sellers/1182')
  expect(summer).toHaveAttribute('href', '/collection/Summer/1183')
})

test("ProductHighlightText should render nothing if there's no context", () => {
  const { container } = render(
    <ProductHighlightText message="{highlightName}" />
  )

  expect(container).toBeEmptyDOMElement()
})

test("ProductHighlightText should render nothing if there's no message", () => {
  const { container } = render(
    // eslint-disable-next-line @typescript-eslint/ban-ts-ignore
    // @ts-ignore
    <ProductHighlightText />
  )

  expect(container).toBeEmptyDOMElement()
})

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of product-highlights/react/ProductHighlightText.tsx from the product-highlights VTEX app.

[Assistant]
Here's the implementation:

```tsx
import type { FC, ReactNode } from 'react'
import React, { useMemo } from 'react'
import { IOMessageWithMarkers, formatIOMessage } from 'vtex.native-types'
import { useCssHandles } from 'vtex.css-handles'
import { useIntl } from 'react-intl'

import { useHighlight } from './ProductHighlights'

interface Props {
  message: string
  markers?: string[]
  blockClass?: string
  link?: string
}

interface MessageValues {
  highlightName: ReactNode
}

const CSS_HANDLES = ['productHighlightText'] as const

const ProductHighlightText: FC<Props> = ({
  message = '',
  markers = [],
  link = '',
}) => {
  const handles = useCssHandles(CSS_HANDLES)
  const value = useHighlight()
  const intl = useIntl()

  const values = useMemo(() => {
    const result: MessageValues = {
      highlightName: '',
    }

    if (!value) {
      return result
    }

    if (link) {
      const href = formatIOMessage(
        {
          intl,
          id: link,
        },
        {
          highlightId: value.highlight.id ?? '',
          highlightName: value.highlight.name,
        }
      ) as string

      result.highlightName = (
        <a
          href={href}
          key="highlightLink"
          data-highlight-name={value.highlight.name}
          data-highlight-id={value.highlight.id}
          data-highlight-type={value.type}
          className={handles.productHighlightText}
        >
          {value.highlight.name}
        </a>
      )
    } else {
      result.highlightName = (
        <span
          key="highlightName"
          data-highlight-name={value.highlight.name}
          data-highlight-id={value.highlight.id}
          data-highlight-type={value.type}
          className={handles.productHighlightText}
        >
          {value.highlight.name}
        </span>
      )
    }

    return result
  }, [value, link, intl, handles.productHighlightText])

  if (!value || !message) {
    return null
  }

  return (
    <IOMessageWithMarkers
      handleBase="productHighlightText"
      message={message}
      markers={markers}
      values={values}
    />
  )
}

export default ProductHighlightText

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of product-highlights/react/ProductHighlightWrapper.test.tsx from the product-highlights VTEX app.

[Assistant]
Here's the implementation:

```tsx
import React from 'react'
import { render } from '@vtex/test-tools/react'
import { useProduct } from 'vtex.product-context'

import ProductHighlights from './ProductHighlights'
import ProductHighlightWrapper from './ProductHighlightWrapper'
import tankTop from './__fixtures__/tankTop'

const mockUseProduct = useProduct as jest.Mock

mockUseProduct.mockImplementation(() => ({ product: tankTop }))

test('ProductHighlight should render data-attributes and CSS handles', () => {
  const { getByText } = render(
    <ProductHighlights
      filter={{ type: 'show', highlightNames: ['Top Sellers'] }}
    >
      <ProductHighlightWrapper>Hello World</ProductHighlightWrapper>
    </ProductHighlights>
  )

  const topSellers = getByText('Hello World')

  expect(topSellers).toHaveAttribute('data-highlight-id', '1182')
  expect(topSellers).toHaveAttribute('data-highlight-name', 'Top Sellers')
  expect(topSellers).toHaveAttribute('data-highlight-type', 'collection')
  expect(topSellers).toHaveClass('productHighlightWrapper')
})

test("ProductHighlight should render nothing if there's no context", () => {
  const { container } = render(
    <ProductHighlightWrapper>Hello World</ProductHighlightWrapper>
  )

  expect(container).toBeEmptyDOMElement()
})

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of product-highlights/react/ProductHighlightWrapper.tsx from the product-highlights VTEX app.

[Assistant]
Here's the implementation:

```tsx
import type { FC, ReactNode } from 'react'
import React from 'react'
import { useCssHandles } from 'vtex.css-handles'

import { useHighlight } from './ProductHighlights'

interface Props {
  blockClass?: string
  children: ReactNode
}

const CSS_HANDLES = ['productHighlightWrapper'] as const

const ProductHighlightWrapper: FC<Props> = ({ children }) => {
  const handles = useCssHandles(CSS_HANDLES)
  const value = useHighlight()

  if (!value) {
    return null
  }

  return (
    <div
      data-highlight-name={value.highlight.name}
      data-highlight-id={value.highlight.id}
      data-highlight-type={value.type}
      className={handles.productHighlightWrapper}
    >
      {children}
    </div>
  )
}

export default ProductHighlightWrapper

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of product-highlights/store/interfaces.json from the product-highlights VTEX app.

[Assistant]
Here's the implementation:

```json
{
  "product-highlights": {
    "component": "ProductHighlights",
    "composition": "children"
  },
  "product-highlight-wrapper": {
    "component": "ProductHighlightWrapper",
    "composition": "children"
  },
  "product-highlight-text": {
    "component": "ProductHighlightText"
  }
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of product-highlights/react/__fixtures__/tankTop.ts from the product-highlights VTEX app.

[Assistant]
Here's the implementation:

```typescript
export default {
  cacheId: 'tank-top',
  productId: '3',
  description: 'Show your arms!',
  productName: 'Tank Top',
  productReference: 'xpto123',
  linkText: 'tank-top',
  brand: 'AOC',
  brandId: '2000012',
  link: 'https://portal.vtexcommercestable.com.br/tank-top/p',
  categories: [
    '/Apparel & Accessories/Clothing/Tops/',
    '/Apparel & Accessories/Clothing/',
    '/Apparel & Accessories/',
  ],
  priceRange: {
    sellingPrice: {
      highPrice: 600,
      lowPrice: 375.6,
    },
    listPrice: {
      highPrice: 1000,
      lowPrice: 600,
    },
  },
  specificationGroups: [
    {
      name: 'Specifications',
      originalName: 'Specifications',
      specifications: [
        {
          name: 'Sleeve type',
          originalName: 'Sleeve type',
          values: ['Sleeveless'],
        },
      ],
    },
    {
      name: 'allSpecifications',
      originalName: 'allSpecifications',
      specifications: [
        {
          name: 'Sleeve type',
          originalName: 'Sleeve type',
          values: ['Sleeveless'],
        },
      ],
    },
  ],
  items: [
    {
      itemId: '3',
      name: 'White',
      nameComplete: 'Tank Top White',
      complementName: '',
      ean: '613468247360',
      variations: [],
      referenceId: [
        {
          Key: 'RefId',
          Value: 'whitet',
        },
      ],
      measurementUnit: 'un',
      unitMultiplier: 1,
      images: [
        {
          cacheId: '155491',
          imageId: '155491',
          imageLabel: '',
          imageTag:
            '<img src="~/arquivos/ids/155491-#width#-#height#/Frame-1.jpg?v=636793841351870000" width="#width#" height="#height#" alt="Frame-1" id="" />',
          imageUrl:
            'https://storecomponents.vtexassets.com/arquivos/ids/155491/Frame-1.jpg?v=636793841351870000',
          imageText: 'Frame-1',
        },
      ],
      sellers: [
        {
          sellerId: '1',
          sellerName: 'VTEX',
          commertialOffer: {
            discountHighlights: [
              {
                name: 'Free Shipping',
              },
              {
                name: '10% Discount',
              },
            ],
            teasers: [
              {
                name: '8% Boleto',
                conditions: {
                  minimumQuantity: 0,
                  parameters: [
                    {
                      name: 'PaymentMethodId',
                      value: '6',
                    },
                  ],
                },
                effects: {
                  parameters: [
                    {
                      name: 'PercentualDiscount',
                      value: '8.0',
                    },
                  ],
                },
              },
              {
                name: '5% Visa',
                conditions: {
                  minimumQuantity: 0,
                  parameters: [
                    {
                      name: 'PaymentMethodId',
                      value: '3',
                    },
                  ],
                },
                effects: {
                  parameters: [
                    {
                      name: 'PercentualDiscount',
                      value: '5.0',
                    },
                  ],
                },
              },
            ],
            Installments: [
              {
                Value: 62.6,
                InterestRate: 0,
                TotalValuePlusInterestRate: 375.6,
                NumberOfInstallments: 6,
                Name: 'Visa 6 vezes sem juros',
                PaymentSystemName: 'Visa',
              },
            ],
            Price: 375.6,
            ListPrice: 600,
            Tax: 0,
            taxPercentage: 0,
            spotPrice: 345.5,
            PriceWithoutDiscount: 375.6,
            RewardValue: 0,
            PriceValidUntil: '2021-09-01T19:59:52.2582922Z',
            AvailableQuantity: 3000000,
          },
        },
      ],
    },
    {
      itemId: '4',
      name: 'Black',
      nameComplete: 'Tank Top Black',
      complementName: '',
      ean: 'code123',
      variations: [],
      referenceId: [
        {
          Key: 'RefId',
          Value: 'code1234',
        },
      ],
      measurementUnit: 'un',
      unitMultiplier: 1,
      images: [
        {
          cacheId: '155490',
          imageId: '155490',
          imageLabel: '',
          imageTag:
            '<img src="~/arquivos/ids/155490-#width#-#height#/Frame-2.jpg?v=636793841164270000" width="#width#" height="#height#" alt="Frame-2" id="" />',
          imageUrl:
            'https://storecomponents.vtexassets.com/arquivos/ids/155490/Frame-2.jpg?v=636793841164270000',
          imageText: 'Frame-2',
        },
      ],
      sellers: [
        {
          sellerId: '1',
          sellerName: 'VTEX',
          commertialOffer: {
            discountHighlights: [],
            teasers: [
              {
                name: '8% Boleto',
                conditions: {
                  minimumQuantity: 0,
                  parameters: [
                    {
                      name: 'PaymentMethodId',
                      value: '6',
                    },
                  ],
                },
                effects: {
                  parameters: [
                    {
                      name: 'PercentualDiscount',
                      value: '8.0',
                    },
                  ],
                },
              },
            ],
            Installments: [
              {
                Value: 100,
                InterestRate: 0,
                TotalValuePlusInterestRate: 600,
                NumberOfInstallments: 6,
                Name: 'Visa 6 vezes sem juros',
                PaymentSystemName: 'Visa',
              },
            ],
            Price: 600,
            ListPrice: 1000,
            Tax: 0,
            taxPercentage: 0,
            spotPrice: 552,
            PriceWithoutDiscount: 600,
            RewardValue: 0,
            PriceValidUntil: '2021-09-01T19:59:52.2625928Z',
            AvailableQuantity: 3000000,
          },
        },
      ],
    },
  ],
  skuSpecifications: null,
  clusterHighlights: [
    {
      id: '1182',
      name: 'Top Sellers',
    },
    {
      id: '1183',
      name: 'Summer',
    },
  ],
  properties: [
    {
      name: 'Sleeve type',
      values: ['Sleeveless'],
    },
  ],
  selectedProperties: null,
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of product-highlights/react/modules/seller.ts from the product-highlights VTEX app.

[Assistant]
Here's the implementation:

```typescript
import type { ProductTypes } from 'vtex.product-context'

export function getSeller(item: ProductTypes.Item) {
  const defaultSeller = item?.sellers?.find((seller) => seller.sellerDefault)

  if (!defaultSeller) {
    return item?.sellers?.[0]
  }

  return defaultSeller
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of cy-runner/node/deprecated.js from the cy-runner VTEX app.

[Assistant]
Here's the implementation:

```javascript
const { get } = require('lodash')

const logger = require('./logger')

module.exports.deprecated = async (config) => {
  // Let's try to warn deprecated features
  const FOUND = []
  const DEPRECATED = [
    'base.vtex.deployCli',
    'base.twilio',
    'base.jira.testing',
    'base.cypress.sorry',
    'workspace.linkApp.logOutput',
  ]

  DEPRECATED.forEach((flag) => {
    if (get(config, flag)) FOUND.push(flag)
  })

  // eslint-disable-next-line vtex/prefer-early-return
  if (FOUND.length) {
    logger.msgSection('Deprecation report')
    logger.msgWarn('Deprecated configuration on your cy-runner.yml')
    FOUND.forEach((flag) => {
      logger.msgPad(flag)
    })
    logger.msgOk('You should remove it as soon as possible')
  }
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of cy-runner/node/logger.js from the cy-runner VTEX app.

[Assistant]
Here's the implementation:

```javascript
const path = require('path')

const storage = require('./storage')
const system = require('./system')

const LOG_PATH = path.join(system.cyRunnerPath(), 'logs')
const LOG_FILE = path.join(LOG_PATH, '_cy-runner.log')
const PUL_FILE = path.join(LOG_PATH, '_cy-decorator.txt')

function ico(type) {
  switch (type) {
    case 'section':
      return '####'

    case 'ok':
      return '[✓]'.padStart(8)

    case 'warn':
      return '[!]'.padStart(8)

    case 'error':
      return '[✗]'.padStart(8)

    case 'pipe':
      return '  ☍'.padStart(10)

    default:
      return '- '.padStart(8)
  }
}

// Init logs, clean before each run if local
exports.init = () => {
  if (system.isCI()) return storage.makeDir(LOG_PATH)
  storage.delete(PUL_FILE)
  storage.delete(LOG_PATH)
  storage.makeDir(LOG_PATH)
}

// Return log path
exports.logPath = () => {
  return LOG_PATH
}

// Return log file
exports.logFile = () => {
  return LOG_FILE
}

// Write messages to log file and console
exports.write = (msg, pr = false) => {
  process.stdout.write(msg)
  storage.append(msg, LOG_FILE)
  // Send pr messages to be printed in GitHub PR
  if (pr) {
    storage.append(
      storage.exists(PUL_FILE) ? msg : `#### Cy-Runner\n\n${msg}`,
      PUL_FILE
    )
  }
}

exports.msgOk = (msg, pr = false) => {
  this.write(`\n${ico('ok')} ${msg}`, pr)
}

exports.msgWarn = (msg, pr = false) => {
  this.write(`\n${ico('warn')} ${msg}`, pr)
}

exports.msgError = (msg, pr = false) => {
  this.write(`\n${ico('error')} ${msg}`, pr)
}

exports.msgPipe = (msg) => {
  this.write(`\n${ico('pipe')} ${msg}`)
}

exports.msgPad = (msg, pr = false) => {
  this.write(`\n${ico()} ${msg}`, pr)
}

exports.msgSection = (msg, pr = false) => {
  if (system.isCI()) {
    // If first section, don't close group
    // eslint-disable-next-line no-console
    if (msg !== 'Cypress Runner') console.log(`::endgroup::`)

    // Open group for each new section
    // eslint-disable-next-line no-console
    console.log(`::group::${msg}`)
  }

  this.write(`\n\n${ico('section')} ${msg}\n`, pr)
}

exports.msgEnd = (msg, pr = false) => {
  this.write(`\n\n${ico('section')} ${msg}\n`, pr)
}

exports.newLine = (n = 1, pr = false) => {
  for (let i = 0; i < n; i++) {
    this.write('\n', pr)
  }
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of cy-runner/node/test.js from the cy-runner VTEX app.

[Assistant]
Here's the implementation:

```javascript
const path = require('path')

const { startCase, difference, intersection } = require('lodash')

const system = require('./system')
const logger = require('./logger')
const cypress = require('./cypress')

let specsFailed = []
let specsSkipped = []
let specsDisabled = []
let specsPassed = []
let runUrl = null

exports.runTests = async (config) => {
  // Control time and load strategies
  const START = system.tick()
  const STRGS = config.strategy

  // Start Xvfb
  const xvfb = await this.startXvfb()

  // Loop strategies
  for (const strategy in STRGS) {
    const test = STRGS[strategy]
    const group = `${system.getId()}/${strategy}`

    test.name = startCase(strategy)

    // If test disabled, just flag and continue
    if (!test.enabled) {
      specsDisabled = specsDisabled.concat(test.specs)
      continue
    }

    // If it has dependencies, and they don't meet, flag and continue
    if (test.dependency?.length) {
      // Set to drop eventual user duplications by mistaken
      const deps = [...new Set(test.dependency)].sort()
      const pass = [...new Set(specsPassed)].sort()
      const miss = difference(deps, pass)

      // Dependencies don't meet
      if (miss?.length) {
        logger.msgSection(`[skip] Strategy ${test.name}`)
        const loopPass = intersection(deps, pass)
        const loopMiss = intersection(deps, miss)
        const loopSkip = intersection(deps, specsSkipped)

        // Show feedback to user
        loopPass.forEach((spec) => {
          logger.msgOk(cypress.specNameClean(spec))
        })
        loopSkip.forEach((spec) => {
          logger.msgWarn(cypress.specNameClean(spec))
        })
        loopMiss.forEach((spec) => {
          logger.msgError(cypress.specNameClean(spec))
        })

        // Feed skipped list
        specsSkipped = specsSkipped.concat(test.specs)

        continue
      }
    }

    // All set, let's run the test
    // eslint-disable-next-line no-await-in-loop
    await runTest(test, config, group)
  }

  // Stops Xvfb
  await this.stopXvfb(xvfb)

  return {
    time: system.tack(START),
    specsFailed,
    specsSkipped,
    specsDisabled,
    specsPassed,
    runUrl,
  }
}

exports.startXvfb = async () => {
  // Xvfb only run if running on CI
  if (system.isCI()) {
    const displayLog = path.join(logger.logPath(), '_display.log')

    return system.spawn(
      'Xvfb',
      ['-screen', '0', '1024x768x24', ':99'],
      displayLog
    )
  }
}

exports.stopXvfb = async (xvfb) => {
  if (system.isCI()) xvfb.kill()
}

async function runTest(test, config, group) {
  const cypressOptions = { parallel: test.parallel }
  const tries = test.hardTries + 1
  let testTry = 1

  test.specs = [...new Set(test.specs)] // Sanitize
  while (testTry <= tries && test.specs.length) {
    logger.msgSection(`[try ${testTry}/${tries}] Strategy ${test.name}`)
    if (runUrl) {
      logger.msgOk('Dashboard URL')
      logger.msgPad(runUrl)
    }

    logger.msgOk('Specs to run on this try')
    test.specs.forEach((spec) => {
      logger.msgPad(cypress.specNameClean(spec))
    })
    cypressOptions.group = `${group}/${testTry}`
    // eslint-disable-next-line no-await-in-loop
    const results = await cypress.run(test, config, cypressOptions)

    // eslint-disable-next-line no-loop-func
    results.forEach((result) => {
      if (!runUrl) runUrl = result.runUrl
      if (result.success) {
        test.specs = difference(test.specs, result.specsPassed)
        specsPassed = specsPassed.concat(result.specsPassed)
      }
    })
    testTry++
  }

  if (!test.specs.length) {
    logger.msgOk(`Strategy ${test.name} ran successfully`)
    specsPassed = [...new Set(specsPassed)].sort() // Sanitization
    specsPassed.forEach((spec) => {
      logger.msgPad(`Passed: ${cypress.specNameClean(spec)}`)
    })
  } else {
    logger.msgError(`Strategy ${test.name} failed`)
    specsFailed = specsFailed.concat(test.specs)
    specsFailed = [...new Set(specsFailed)].sort() // Sanitization
    specsFailed.forEach((spec) => {
      logger.msgPad(`Failed: ${cypress.specNameClean(spec)}`)
    })
    if (test.stopOnFail) {
      await cypress.stopOnFail(
        config,
        { strategy: test.name, specsPassed, specsFailed },
        runUrl
      )
    }
  }
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of cy-runner/node/system.js from the cy-runner VTEX app.

[Assistant]
Here's the implementation:

```javascript
const { execSync, spawn } = require('child_process')
const fs = require('fs')
const path = require('path')

const logger = require('./logger')
const storage = require('./storage')
const workspace = require('./workspace')

const BASE_PATH = path.join(__dirname, '..', '..')

// Return base path
exports.basePath = () => {
  return BASE_PATH
}

// Return root path
exports.rootPath = () => {
  return path.join(BASE_PATH, '..')
}

// Return cy-runner path
exports.cyRunnerPath = () => {
  return path.join(BASE_PATH, 'cy-runner')
}

// Return toolbelt to be used
exports.vtexBin = async (crash = false) => {
  const VTEX_BIN = path.join(
    this.cyRunnerPath(),
    'node_modules',
    'vtex',
    'bin',
    'run'
  )

  // Return the installed as cy-runner package, for sure it is on the right version
  if (storage.exists(VTEX_BIN)) return VTEX_BIN

  // Let's avoid check version every time
  if (crash) {
    const VTEX_VERSION = this.exec('vtex version', 'pipe')
    const check = /beta-ci/.test(VTEX_VERSION)

    return check
      ? 'vtex'
      : this.crash("Run 'yarn install' inside cy-runner folder", VTEX_VERSION)
  }

  // It passed on crash test already, just return
  return 'vtex'
}

// Detect CI
exports.isCI = () => {
  return !!process.env.CI
}

// Debug file
exports.debugFile = () => {
  return path.join(process.env.HOME, '.vtex', 'logs', 'debug.json')
}

// Delay
exports.delay = (ms) => new Promise((res) => setTimeout(res, ms))

// Crash and exit
exports.crash = (msg, err, options = { pr: true, dump: true }) => {
  logger.msgEnd('Error', options.pr)
  logger.msgError(msg, 'Crash', options.pr)
  if (typeof err !== 'undefined') logger.msgPad(err, options.pr)

  // kill any subprocesses created by cy-runner
  const PID_FILE = path.join(logger.logPath(), '_pid')

  if (storage.exists(PID_FILE)) {
    const PID = storage.read(PID_FILE)

    logger.msgPad(`Killing pid ${PID} [vtex link]`)
    try {
      process.kill(Number(PID.toString()), 9)
    } catch (e) {
      logger.msgPad(`Failed to kill ${PID}`)
    }
  }

  // Dump env
  if (options.dump) workspace.dumpEnvironment().then((r) => r)

  logger.newLine(2)

  // Give GitHub information to print about the error
  // eslint-disable-next-line no-console
  if (options.pr) console.log(`::error title=${msg}::${err}`)
  process.exit(99)
}

// Success and exit
exports.success = (msg) => {
  logger.msgEnd('Success', true)
  logger.msgOk(msg, true)
  logger.newLine()
  process.exit(0)
}

// Fail and exit
exports.fail = (msg) => {
  logger.msgEnd('Fail', true)
  logger.msgError(msg, true)
  logger.newLine()
  process.exit(17)
}

// Run sync process
exports.exec = (cmd, output = 'ignore', cwd = process.cwd()) => {
  const timeout = 5 * 60 * 1000 // 5 minutes
  let result

  try {
    result = execSync(cmd, { stdio: output, timeout, cwd })
  } catch (e) {
    logger.msgError(`Failed to run ${cmd}`)
    logger.msgPad(e)
    result = 'error'

    // If timeout, exit
    if (/ETIMEDOUT/.test(e)) this.crash(`Timeout running ${cmd}`, e)
  }

  return result.toString()
}

// Start a background process
// eslint-disable-next-line max-params
exports.spawn = (bin, cmd, logFile, cwd = process.cwd()) => {
  const out = fs.openSync(logFile, 'a')
  const err = fs.openSync(logFile, 'a')

  try {
    const subprocess = spawn(bin, cmd, {
      cwd,
      detached: true,
      stdio: ['ignore', out, err],
    })

    subprocess.unref()

    return subprocess
  } catch (e) {
    logger.msgError(`Failed to spawn ${cmd}`)
    logger.msgPad(e)
  }
}

// Start timer
exports.tick = () => {
  return Date.now()
}

// Finish time
exports.tack = (start) => {
  const stop = (Date.now() - start) / 1000
  const convert = (x) => (x < 10 ? `0${Math.floor(x)}` : Math.floor(x))
  const h = convert(stop / (60 * 60))
  const m = convert((stop / 60) % 60)
  const s = convert(stop % 60)

  return `${h}:${m}:${s}`
}

// Init ID
exports.initId = () => {
  const ID = this.tick().toString().substring(6, 13)
  const FILE_ID = path.join(logger.logPath(), '_id')

  storage.delete(FILE_ID)
  storage.write(ID, FILE_ID)
}

// Get ID
exports.getId = () => {
  const FILE_ID = path.join(logger.logPath(), '_id')

  if (storage.exists(FILE_ID)) return storage.read(FILE_ID)

  this.initId()

  return storage.read(FILE_ID)
}

// Traverse YAML or JSON
exports.traverse = (result, obj, previousKey) => {
  if (typeof obj === 'object') {
    for (const key in obj) {
      this.traverse(
        result,
        obj[key],
        (previousKey || '') + (previousKey ? `.${key}` : key)
      )
    }
  } else {
    result.push({
      key: previousKey || '',
      type: obj,
    })
  }

  return result
}

// Check mixed paths
exports.checkSpecHealth = async (config) => {
  for (const strategy in config.strategy) {
    const test = config.strategy[strategy]

    if (test.enabled) {
      const PATH = path.parse(test.specs[0]).dir

      // eslint-disable-next-line no-loop-func
      test.specs.forEach((spec) => {
        if (path.parse(spec).dir !== PATH) {
          this.crash('Paths mixed on the same strategy', spec, {
            dump: false,
          })
        }

        if (!storage.exists(path.join(this.cyRunnerPath(), spec))) {
          this.crash('Spec does not exist', spec, {
            dump: false,
          })
        }
      })
    }
  }

  if (config.workspace.wipe.enabled) {
    const { specs } = config.workspace.wipe

    specs.forEach((spec) => {
      if (!storage.exists(path.join(this.cyRunnerPath(), spec))) {
        this.crash('Wipe spec does not exist', spec, {
          dump: false,
        })
      }
    })
  }
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of cy-runner/node/credentials.js from the cy-runner VTEX app.

[Assistant]
Here's the implementation:

```javascript
const path = require('path')

const { merge } = require('lodash')
const jsYaml = require('js-yaml')

const http = require('./http')
const logger = require('./logger')
const schema = require('./schema')
const system = require('./system')
const toolbelt = require('./toolbelt')
const storage = require('./storage')

exports.getCookies = async (config) => {
  // eslint-disable-next-line vtex/prefer-early-return
  if (config.base.cypress.getCookies) {
    // Admin cookie
    logger.msgOk('Getting admin cookie')
    const axiosConfig = {
      url: config.base.vtex.vtexIdUrl,
      method: 'get',
      params: {
        user: config.base.vtex.apiKey,
        pass: config.base.vtex.apiToken,
      },
    }

    const response = await http.request(axiosConfig)

    if (response?.data?.authStatus !== 'Success') {
      system.crash(
        'Failed to get admin credentials',
        'Check your apiToken and apiKey on your secrets and enable it'
      )
    }

    config.base.vtex.authCookieName = response?.data?.authCookie?.Name
    config.base.vtex.adminAuthCookieValue = response?.data?.authCookie?.Value

    // User cookie
    logger.msgOk('Getting user cookie')
    const userOrRobot = await toolbelt.getLocalToken()

    config.base.vtex.userAuthCookieValue = userOrRobot.token
    logger.msgPad(userOrRobot.mailOrKey)

    return config
  }
}

exports.readSecrets = (config) => {
  if (!config.base.secrets.enabled) {
    logger.msgWarn('Secrets disabled')

    return {}
  }

  logger.msgOk('Loading secrets')
  const SECRET_NAME = config.base.secrets.name
  const SECRET_FILE = path.join(system.cyRunnerPath(), `.${SECRET_NAME}.json`)
  let secrets = process.env[SECRET_NAME]
  let loadedFrom = null

  if (storage.exists(SECRET_FILE)) {
    secrets = storage.readYaml(SECRET_FILE)
    loadedFrom = `file ${SECRET_FILE}`
  } else {
    if (!secrets) {
      system.crash(
        'Secrets missing',
        `You should disable secrets, create a '.${SECRET_NAME}.json' or set a '${SECRET_NAME}' env`
      )
    }

    try {
      secrets = jsYaml.load(secrets, 'utf-8')
      loadedFrom = `env variable ${SECRET_NAME}`
    } catch (e) {
      this.crash(`Check if your env variable ${SECRET_NAME} is well formatted`)
    }
  }

  schema.validateSecrets(secrets, config)
  logger.msgPad(`from ${loadedFrom.replace(system.basePath(), '.')}`)

  return secrets
}

exports.mergeSecrets = (config, secrets) => {
  merge(config.base, secrets)

  return config
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of cy-runner/node/config.js from the cy-runner VTEX app.

[Assistant]
Here's the implementation:

```javascript
const path = require('path')

const { get } = require('lodash')

const logger = require('./logger')
const storage = require('./storage')
const credentials = require('./credentials')
const lock = require('./lock')
const system = require('./system')
const cypress = require('./cypress')
const http = require('./http')

exports.getConfig = async (configFile) => {
  // Load and parse cy-runner.yml file
  logger.msgOk('Loading cy-runner configuration')
  let config = storage.loadConfig(configFile)

  // Make link if base has Cypress folder
  const lnk = path.join(__dirname, '..', 'cypress')
  const src = path.join(__dirname, '..', 'cypress-shared', 'support', 'common')
  const cyp = path.join(__dirname, '..', '..', 'cypress')
  const dst = path.join(__dirname, '..', '..', 'cypress', 'support')

  if (storage.exists(dst)) {
    logger.msgOk('Linking local Cypress code on Cy-Runner')

    // Create common links inside cypress
    const common = path.join(dst, 'common')
    const clean = path.join(__dirname, '..', '..')

    logger.msgPad(`${src.replace(clean, '.')} -> ${common.replace(clean, '.')}`)
    if (storage.exists(common)) storage.unLink(common)
    storage.link(src, common)

    // Create cypress link inside cy-runner
    logger.msgPad(`${cyp.replace(clean, '.')} -> ${lnk.replace(clean, '.')}`)
    if (storage.exists(lnk)) storage.unLink(lnk)
    storage.link(cyp, lnk)
  }

  // Check mixed paths and missing specs
  logger.msgOk('Checking for mixed paths and missing specs')
  await system.checkSpecHealth(config)

  // Check toolbelt version
  logger.msgOk('Checking toolbelt version')
  await system.vtexBin(true)

  // Fill URLs
  const VTEX_AUTH_PATH = '.myvtex.com/api/vtexid/pub/authentication'
  const VTEX_ACCOUNT = config.base.vtex.account
  const WORKSPACE = await this.getWorkspaceName(config)
  const ACCOUNT = config.base.vtex.account
  const DOMAIN = config.base.vtex.domain
  const RESERVE = config.workspace?.reserveAccount?.enabled

  config.base.vtex.authUrl = `https://${VTEX_ACCOUNT}${VTEX_AUTH_PATH}`
  config.base.vtex.baseUrl = `https://${WORKSPACE}--${ACCOUNT}.${DOMAIN}`

  // Load and parse secrets
  const secrets = credentials.readSecrets(config)

  // Lock the orderForm, in CI the lock is done by a step on GH Actions
  if (RESERVE) await lock.reserveAccount(config, secrets)

  // Seed envs if not present
  if (!config.envs) config.envs = []

  // TODO: Migrate this to its own yaml file
  config.base.stateFiles.push('_orderFormDebug.json')
  config.base.keepStateFiles = true

  // Run in different mode if on GitHub (CI)
  if (system.isCI()) {
    logger.msgWarn('Running in CI mode')
    // Enforce config to avoid misconfiguration
    config.base.vtex.isCI = true
    config.base.cypress.devMode = false
    config.base.cypress.runHeaded = false
    config.base.cypress.quiet = true
    config.base.cypress.videoUploadOnPasses = false
    config.base.cypress.trashAssetsBeforeRuns = false
    config.base.cypress.watchForFileChanges = false
    config.base.cypress.browser = 'chrome'
    // Inject envs
    config.envs.push(
      'ELECTRON_EXTRA_LAUNCH_ARGS: --disable-gpu --disable-software-rasterizer'
    )
    config.envs.push('LIBVA_DRIVER_NAME: --disable-software-rasterizer')
    config.envs.push('DISPLAY: :99')
    config.envs.push('NODE_NO_WARNINGS: 1')
  } else {
    config.base.vtex.isCI = false
    // Clean debug
    logger.msgOk('Cleaning debug file')
    storage.delete(system.debugFile())
    logger.msgPad(`${system.debugFile()} cleaned successfully`)

    // If local and not on dev mode
    if (!config.base.cypress.devMode) {
      // Disable parallelism if Sorry Cypress is not running
      const sorryRunning = await http.runningSorryCypress()

      if (!sorryRunning) {
        logger.msgError('Sorry Cypress not detected')
        logger.msgPad('disabling dashboard')
        logger.msgPad('disabling parallelization')
        logger.msgPad('waiting 7 seconds, so you can cancel and try again')
        await system.delay(7000)
        config.base.cypress.maxJobs = 0
      }
    }
  }

  // Merge secrets on config
  config = credentials.mergeSecrets(config, secrets)

  // Report configuration to help understand that'll run
  await this.sectionsToRun(config)

  // Set up Cypress environment
  logger.msgSection('Cypress set up')
  config = await credentials.getCookies(config)
  logger.msgOk('Creating Cypress environment')
  cypress.saveCypressEnvJson(config)
  cypress.saveCypressJson(config)

  // Create state files
  storage.createStateFiles(config)

  // Export envs to make debug easier inside GitHub
  if (config.envs.length) logger.msgOk('Exporting envs variables')
  config.envs.forEach((env) => {
    const [envName, envValue] = env.split(': ')

    logger.msgPad(`${envName} = ${envValue}`)
    process.env[envName] = envValue
  })

  return config
}

exports.getWorkspaceName = async (config) => {
  const { workspace } = config

  if (workspace.name === 'random') {
    const id = system.getId()
    const { prefix } = workspace

    workspace.name = `${prefix}${id}`
  } else {
    workspace.name = workspace.name.toLowerCase()
  }

  logger.msgOk('Defining workspace')
  logger.msgPad(workspace.name)

  return workspace.name
}

exports.sectionsToRun = async (config) => {
  logger.msgSection('Sections to run')
  const getList = (item, property) => {
    const list = get(config, `${item}.${property}`)

    return list !== undefined ? list : []
  }

  system.traverse([], config).forEach((item) => {
    // eslint-disable-next-line vtex/prefer-early-return
    if (/enabled/.test(item.key) && /true/.test(item.type)) {
      const [itemEnabled] = item.key.split('.enabled')

      logger.msgOk(itemEnabled)
      getList(itemEnabled, 'specs').forEach((spec) => {
        logger.msgPad(cypress.specNameClean(spec))
      })
      getList(itemEnabled, 'dependency').forEach((dep) => {
        logger.msgPipe(cypress.specNameClean(dep))
      })
    }
  })

  const NUM_APPS_INSTALL = config.workspace.installApps.length
  const NUM_APPS_REMOVE = config.workspace.removeApps.length

  if (NUM_APPS_INSTALL) {
    logger.msgOk('Apps to be installed')
    getList('workspace', 'installApps').forEach((app) => {
      logger.msgPad(app)
    })
  }

  if (NUM_APPS_REMOVE) {
    logger.msgOk('Apps to be removed')
    getList('workspace', 'removeApps').forEach((app) => {
      logger.msgPad(app)
    })
  }
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of cy-runner/node/workspace.js from the cy-runner VTEX app.

[Assistant]
Here's the implementation:

```javascript
const path = require('path')

const lock = require('./lock')
const system = require('./system')
const logger = require('./logger')
const toolbelt = require('./toolbelt')
const storage = require('./storage')
const http = require('./http')
const { wipe } = require('./wipe')

const MAX_RETRIES = 3

exports.init = async (config) => {
  const START = system.tick()
  const NAME = config.workspace.name

  logger.msgSection('Workspace set up')
  logger.msgOk('Changing workspace')
  logger.msgPad(NAME)

  // Create namespace
  const check = await toolbelt.changeWorkspace(NAME)

  if (!check) system.crash('Failed to change workspace')

  return system.tack(START)
}

exports.installApps = async (config) => {
  const START = system.tick()
  const APPS = config.workspace.installApps

  if (APPS?.length) {
    let check = { success: false, log: null }
    let thisTry = 1

    while (thisTry <= MAX_RETRIES && !check.success) {
      logger.msgOk(`[try ${thisTry}/${MAX_RETRIES}] Installing apps`)
      // eslint-disable-next-line no-await-in-loop
      check = await toolbelt.install(APPS)
      thisTry++
    }

    if (!check.success) system.crash('Failed to install some app', check.log)
  }

  return system.tack(START)
}

exports.uninstallApps = async (config) => {
  const START = system.tick()
  const APPS = config.workspace.removeApps

  if (APPS?.length) {
    let check = { success: false, log: null }
    let thisTry = 1

    while (thisTry <= MAX_RETRIES && !check.success) {
      logger.msgOk(`[try ${thisTry}/${MAX_RETRIES}] Uninstalling apps`)
      // eslint-disable-next-line no-await-in-loop
      check = await toolbelt.uninstall(APPS)
      thisTry++
    }

    if (!check.success) system.crash('Failed to uninstall some app', check.log)
  }

  return system.tack(START)
}

exports.updateVtexIgnore = async () => {
  const IGNORE_FILE = path.join(system.basePath(), '.vtexignore')
  const SHORT_NAME = IGNORE_FILE.replace(system.rootPath(), '.')

  logger.msgOk(`Updating ${SHORT_NAME}`)
  if (!storage.exists(IGNORE_FILE)) storage.write('# cy-r\n', IGNORE_FILE)
  const EXCLUSIONS = ['cypress', 'cy-runner', 'cypress-shared']

  EXCLUSIONS.forEach((exclusion) => {
    storage.append(`${exclusion}\n`, IGNORE_FILE)
  })
}

exports.linkApp = async (config) => {
  const START = system.tick()

  // eslint-disable-next-line vtex/prefer-early-return
  if (config.workspace.linkApp.enabled) {
    const MANIFEST_FILE = path.join(system.basePath(), 'manifest.json')
    const SHORT_NAME = MANIFEST_FILE.replace(system.rootPath(), '.')

    // Update vtex ignore
    await this.updateVtexIgnore()

    // Read name of app to be linked
    logger.msgOk(`Reading ${SHORT_NAME}`)
    const MANIFEST = storage.readYaml(MANIFEST_FILE)
    const APP = `${MANIFEST.vendor}.${MANIFEST.name}@${MANIFEST.version}`

    // Link app
    logger.msgOk(`Linking ${APP}`)
    let APP_LOG = path.join(logger.logPath(), `_link_${APP}.log`)
    const APP_PID = path.join(logger.logPath(), `_pid`)
    const STOP = 13
    const link = await toolbelt.link(APP_LOG)
    let check = false
    let loop = 0

    while (!check) {
      // To not wait forever
      loop++
      if (loop === STOP) break
      if (link.killed) system.crash('Link failed', JSON.stringify(link))

      logger.msgPad(`waiting ${130 - loop * 10} seconds until link gets ready`)
      // eslint-disable-next-line no-await-in-loop
      await system.delay(10000)

      const log = storage.read(APP_LOG)

      check = /App running | App linked successfully/.test(log.toString())
    }

    if (check) {
      logger.msgOk('App linked successfully')
    } else {
      APP_LOG = APP_LOG.replace(system.cyRunnerPath(), '.')
      logger.msgError('Failed to link app, you should', true)
      logger.msgPad(`1. Read the log ${APP_LOG}`, true)
      logger.msgPad('2. Fix your code if get some error on log', true)
      logger.msgPad('3. Try the E2E test again', true)
      logger.msgWarn('The link fail due an error or timeout (120 s)', true)
    }

    if (link.pid) storage.write(link.pid.toString(), APP_PID)

    // Test login page after app link
    this.checkLoginPage(config.workspace.name)

    return { success: check, time: system.tack(START), subprocess: link }
  }

  return { success: true, time: system.tack(START), subprocess: null }
}

exports.checkLoginPage = async (workspace) => {
  const LOGIN_PATH = '/_v/segment/admin-login/v1/login'
  const AXIOS_CFG = {
    url: `https://${workspace}--productusqa.myvtex.com${LOGIN_PATH}`,
    method: 'get',
  }

  let thisTry = 1
  let check = false

  while (thisTry <= MAX_RETRIES && !check) {
    logger.msgOk(`[try ${thisTry}/${MAX_RETRIES}] Checking login page`)

    // eslint-disable-next-line no-await-in-loop
    const response = await http.request(AXIOS_CFG)

    if (/2../.test(response?.status)) check = true
    thisTry++
  }

  if (!check) system.crash('Access to login page failed')
}

exports.teardown = async (config, linkSucceed = true) => {
  const START = system.tick()
  const { workspace } = config
  const RESERVE = config.workspace?.reserveAccount?.enabled

  if (RESERVE) await lock.releaseAccount(config)

  logger.msgSection('Workspace teardown')
  await this.dumpEnvironment()
  if (config.base.keepStateFiles) storage.keepStateFiles(config)
  // Run wipe only if link succeeds
  if (linkSucceed) await wipe(config)
  await this.cleanSensitiveData()
  if (workspace.teardown.enabled) await toolbelt.deleteWorkspace(workspace.name)

  return system.tack(START)
}

// Save apps list
exports.dumpEnvironment = async () => {
  const APPS_INSTALLED = path.join(logger.logPath(), '_apps_installed.txt')
  const APPS_DEPENDENCY = path.join(logger.logPath(), '_apps_dependency.json')

  logger.msgOk('Dumping environment')
  logger.msgPad(APPS_INSTALLED.replace(system.basePath(), '.'))
  storage.write((await toolbelt.ls()).toString(), APPS_INSTALLED)
  logger.msgPad(APPS_DEPENDENCY.replace(system.basePath(), '.'))
  storage.write((await toolbelt.dependency()).toString(), APPS_DEPENDENCY)

  const SRC = system.debugFile()
  let DST = path.join(logger.logPath(), '_debug.json')

  logger.msgPad(`${SRC} -> ${DST.replace(system.basePath(), '.')}`)
  storage.copy(SRC, DST)

  DST = path.join(logger.logPath(), '_node_versions.json')
  logger.msgPad(`Node versions -> ${DST.replace(system.basePath(), '.')}`)
  storage.append(JSON.stringify(process.versions), DST)

  DST = path.join(logger.logPath(), '_env.txt')
  logger.msgPad(`Env variables -> ${DST.replace(system.basePath(), '.')}`)
  delete process.env.VTEX_QE
  storage.append(JSON.stringify(process.env), DST)
}

// Clean sensitive data
exports.cleanSensitiveData = async () => {
  const SENSITIVE_FILES = ['cypress.env.json', 'cypress.json']

  logger.msgOk('Cleaning sensitive data')
  SENSITIVE_FILES.forEach((file) => {
    logger.msgPad(file)
    storage.delete(file)
  })
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of cy-runner/node/schema.js from the cy-runner VTEX app.

[Assistant]
Here's the implementation:

```javascript
/** ********************************************
 Validate schema on cy-runner.yml
 Test only mandatory fields cy-runner works
 0 = String not null
 1 = Integer not null
 2 = Boolean not null
 3 = String
 4 = Integer
 5 = Boolean
 6 = Not null
 7 = array
 ********************************************* */

const { get } = require('lodash')

const system = require('./system')

function schemaValidator(schema, config, strategy = '') {
  const skip = []
  const schemaTraversed = system.traverse([], schema)
  const ignore = (key, value) => {
    let byPass = null

    key = strategy + key
    if (/\.enabled/.test(key) && typeof value === 'boolean' && !value) {
      skip.push(key.split('.enabled')[0])
    }

    skip.forEach((disabled) => {
      byPass = !!key.includes(disabled)
    })

    return byPass
  }

  schemaTraversed.forEach((item) => {
    const value = get(config, item.key)
    let crash = false
    let msg = 'not null'

    if (ignore(item.key, value)) return
    if (/[0126]/.test(item.type) && value == null) crash = true
    switch (item.type) {
      // String
      case 0:
      /* falls through */

      case 3:
        msg = 'string'
        crash = typeof value !== msg
        break

      // Integer
      case 1:
      /* falls through */

      case 4:
        msg = 'number'
        crash = typeof value !== msg
        break

      // Boolean
      case 2:
      /* falls through */

      case 5:
        msg = 'boolean'
        crash = typeof value !== msg
        break

      // Array
      case 7:
        msg = 'array'
        try {
          // If not array, it'll fail
          // noinspection JSObjectNullOrUndefined
          value.push('isThisArray?')
          value.pop()
        } catch (e) {
          crash = true
        }

        break

      default:
        break
    }

    if (crash) {
      system.crash(
        'Parse of cy-runner.yml failed',
        `${strategy}${item.key} must be ${msg}`
      )
    }
  })
}

exports.validateConfig = (config) => {
  const BASE_SCHEMA = {
    base: {
      secrets: {
        enabled: 2,
        name: 0,
      },
      vtex: {
        account: 0,
        id: 4,
        domain: 0,
      },
      cypress: {
        devMode: 2,
        runHeaded: 2,
        getCookies: 2,
        maxJobs: 1,
        quiet: 2,
        projectId: 0,
        video: 2,
        videoCompression: 6,
        videoUploadOnPasses: 2,
        screenshotOnRunFailure: 2,
        trashAssetsBeforeRuns: 2,
        viewportWidth: 1,
        viewportHeight: 1,
        defaultCommandTimeout: 1,
        requestTimeout: 1,
        watchForFileChanges: 2,
        pageLoadTimeout: 1,
        browser: 0,
        chromeWebSecurity: 2,
      },
      jira: { enabled: 2, account: 0, board: 0, issueType: 0 },
      slack: { enabled: 2, channel: 3 },
      stateFiles: 7,
    },
    workspace: {
      name: 3,
      prefix: 0,
      linkApp: {
        enabled: 2,
      },
      installApps: 7,
      removeApps: 7,
      wipe: { enabled: 2, stopOnFail: 2, specs: 7 },
      teardown: { enabled: 2 },
    },
  }

  const STRATEGY_SCHEMA = {
    enabled: 2,
    sendDashboard: 2,
    stopOnFail: 2,
    parallel: 2,
    hardTries: 1,
    specs: 7,
  }

  // Validate base
  schemaValidator(BASE_SCHEMA, config)

  // Validate test strategies
  const configSchema = {}

  Object.entries(config.strategy).forEach((entry) => {
    configSchema[entry[0]] = entry[1]
  })
  Object.keys(configSchema).forEach((strategy) => {
    schemaValidator(STRATEGY_SCHEMA, configSchema[strategy], `${strategy}.`)
  })

  // Validate dependencies
  checkDependency(config)

  // All set, show the user a positive feedback
  return true
}

exports.validateSecrets = (secrets, config) => {
  try {
    if (config.base.secrets.enabled) {
      const VTEX_ATTRIBUTES = [
        'apiKey',
        'apiToken',
        'authCookieName',
        'robotMail',
        'robotPassword',
      ]

      VTEX_ATTRIBUTES.forEach((att) => {
        checkSecret(`secrets.vtex.${att}`, secrets.vtex[att])
      })
    }
  } catch (e) {
    system.crash('This property is missing on your secrets', e)
  }
}

// Check secrets
function checkSecret(key, value) {
  key = key.split('secrets.')[1]
  if (typeof value !== 'string') system.crash('This secret must be string', key)
  if (value.length <= 0) system.crash("This secret can't be null", key)
}

// Check dependencies
function checkDependency(config) {
  const dependencies = []
  const specs = []
  const missing = []

  // Get dependencies and all specs
  system.traverse([], config.strategy).forEach((item) => {
    if (/.dependency./.test(item.key)) dependencies.push(item.type)
    else if (/.spec.js/.test(item.type)) specs.push(item.type)
  })

  // Check if dependency is also a spec
  dependencies.forEach((item) => {
    if (!specs.includes(item)) missing.push(item)
  })

  if (missing.length) {
    const text = missing.length > 1 ? 'Dependencies' : 'Dependency'

    system.crash(`${text} missing on strategies`, JSON.stringify(missing))
  }
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of cy-runner/node/report.js from the cy-runner VTEX app.

[Assistant]
Here's the implementation:

```javascript
const logger = require('./logger')
const system = require('./system')
const cypress = require('./cypress')

module.exports.report = async (control, config) => {
  logger.msgSection('Execution report')

  logger.msgOk('Execution time', true)

  for (const section in control.timing) {
    logger.msgPad(`${section.padEnd(30, '.')} ${control.timing[section]}`, true)
  }

  logger.newLine(1)

  if (config.base.cypress.devMode) {
    system.success('Hope your tests went well. See you soon!')
  } else {
    this.printSpecs(control)

    if (control.runUrl != null) cypress.showDashboard(control.runUrl)

    // Final result
    if (
      control.specsFailed?.length < 1 &&
      control.specsSkipped?.length < 1 &&
      control.specsPassed?.length > 0
    ) {
      system.success('The test ran successfully, well done!')
    } else {
      const url =
        'https://www.notion.so/vtexhandbook/My-PR-got-failed-what-s-next'

      logger.msgSection(`[Notion documentation](${url})`, true)
      logger.msgOk(url, true)
      system.fail('The test failed! Please, check the artifacts.')
    }
  }
}

module.exports.printSpecs = (control) => {
  const items = [
    ['specsDisabled', 'Disabled', 'ok'],
    ['specsPassed', 'Successful', 'ok'],
    ['specsSkipped', 'Skipped', 'warn'],
    ['specsFailed', 'Failed', 'error'],
  ]

  items.forEach((item) => {
    const tests = control[item[0]]

    // eslint-disable-next-line vtex/prefer-early-return
    if (tests?.length > 0) {
      const str = tests.length > 1 ? 'specs' : 'spec'

      if (item[2] === 'ok') logger.msgOk(`${item[1]} ${str}`)
      if (item[2] === 'warn') logger.msgWarn(`${item[1]} ${str}`)
      if (item[2] === 'error') logger.msgError(`${item[1]} ${str}`)
      tests.sort()
      tests.forEach((test) => {
        logger.msgPad(cypress.specNameClean(test))
      })

      logger.newLine(1)
    }
  })
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of cy-runner/node/toolbelt.js from the cy-runner VTEX app.

[Assistant]
Here's the implementation:

```javascript
const system = require('./system')
const logger = require('./logger')

const MAX_RETRIES = 3

// Get user, email and workspace name
exports.whoami = async () => {
  const VTEX = await system.vtexBin()
  let stdout = null
  let check = false
  let thisTry = 1

  while (thisTry <= MAX_RETRIES && !check) {
    stdout = system.exec(`${VTEX} whoami`, 'pipe').toString()
    check = /Logged/.test(stdout)
    thisTry++
  }

  const mailOrKey = check ? stdout.split(' ')[7] : null
  const workspace = check ? stdout.split(' ')[11] : null

  return { isLogged: check, mailOrKey, workspace, stdout }
}

// Get local user token
exports.getLocalToken = async () => {
  const VTEX = await system.vtexBin()
  const userOrRobot = await this.whoami()
  let result = null
  let token = null

  if (userOrRobot.isLogged) {
    let check = false
    let thisTry = 1

    while (thisTry <= MAX_RETRIES && !check) {
      result = system.exec(`${VTEX} local token`, 'pipe')
      token = result.slice(0, -1)
      check = /ey/.test(token)
      thisTry++
    }

    userOrRobot.token = token

    if (check) return userOrRobot
  }

  system.crash(
    'Failed to get local user token',
    'Are you logged on VTEX Toolbelt?'
  )
}

// Crash app if on master
exports.crashIfMaster = async (msg) => {
  const check = await this.whoami()

  if (check.workspace === 'master') {
    system.crash(`Failed to ${msg}`, `You can't ${msg} master workspace`)
  }
}

exports.deleteWorkspace = async (workspace) => {
  await this.crashIfMaster('delete')
  const VTEX = await system.vtexBin()

  const result = system.exec(`${VTEX} workspace delete -f ${workspace}`, 'pipe')
  const check = /successfully/.test(result)

  check
    ? logger.msgOk(`Workspace ${workspace} deleted`)
    : logger.msgPad(`Failed to delete workspace ${workspace}`)
}

exports.changeWorkspace = async (workspace) => {
  const VTEX = await system.vtexBin()
  let check = false
  let thisTry = 1

  while (thisTry <= MAX_RETRIES && !check) {
    system.exec(`${VTEX} workspace use ${workspace}`, 'pipe')
    // eslint-disable-next-line no-await-in-loop
    const result = await this.whoami()

    check = RegExp(result.workspace).test(workspace)
    thisTry++
  }

  return check
}

exports.ls = async () => {
  const VTEX = await system.vtexBin()

  return system.exec(`${VTEX} ls`, 'pipe').toString()
}

exports.dependency = async () => {
  const VTEX = await system.vtexBin()

  return system.exec(`${VTEX} deps ls`, 'pipe').toString()
}

exports.install = async (app) => {
  // Show list of apps to be installed
  app.forEach((a) => {
    logger.msgPad(a)
  })
  // Crash if on master
  await this.crashIfMaster('install apps')
  const VTEX = await system.vtexBin()

  // Install apps and count successfully ones
  const result = system.exec(`${VTEX} install ${app.join(' ')}`, 'pipe')
  const count = (result.match(/successfully/g) || []).length

  // Feedback
  count === app.length
    ? logger.msgOk(`${count} of ${app.length} apps installed`)
    : logger.msgError(`${count} of ${app.length} apps installed`)

  // Return success if the count matches apps requested to install
  return { success: count === app.length, log: result }
}

exports.uninstall = async (app) => {
  // Show list of apps to be installed
  app.forEach((a) => {
    logger.msgPad(a)
  })
  // Crash if on master
  await this.crashIfMaster('uninstall apps')
  const VTEX = await system.vtexBin()

  // Install apps and count successfully ones
  const result = system.exec(`${VTEX} uninstall ${app.join(' ')}`, 'pipe')
  const count = (result.match(/successfully/g) || []).length

  // Feedback
  count === app.length
    ? logger.msgOk(`${count} of ${app.length} apps uninstalled`)
    : logger.msgError(`${count} of ${app.length} apps uninstalled`)

  // Return success if the count matches apps requested to uninstall
  return { success: count === app.length, log: result }
}

exports.link = async (logFile) => {
  const VTEX = await system.vtexBin()

  return system.spawn(VTEX, ['link', '--verbose'], logFile, system.basePath())
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of cy-runner/node/cypress.js from the cy-runner VTEX app.

[Assistant]
Here's the implementation:

```javascript
const path = require('path')

const cypress = require('cypress')
const { merge } = require('lodash')
const jsYaml = require('js-yaml')

const storage = require(`./storage`)
const system = require('./system')
const logger = require('./logger')
const workspace = require('./workspace')

// Get Cypress Folder [cypress or cypress-shared]
exports.getCypressFolder = () => {
  return storage.exists(path.join(system.basePath(), 'cypress', 'integration'))
    ? 'cypress'
    : 'cypress-shared'
}

// Save Env for Cypress
exports.saveCypressEnvJson = (config) => {
  storage.writeJson(
    config,
    path.join(system.cyRunnerPath(), 'cypress.env.json')
  )
}

// Save config for Cypress
exports.saveCypressJson = (config) => {
  storage.writeJson(
    {
      baseUrl: config.base.vtex.baseUrl,
      chromeWebSecurity: config.base.cypress.chromeWebSecurity,
      video: config.base.cypress.video,
      videoCompression: config.base.cypress.videoCompression,
      videoUploadOnPasses: config.base.cypress.videoUploadOnPasses,
      screenshotOnRunFailure: config.base.cypress.screenshotOnRunFailure,
      trashAssetsBeforeRuns: config.base.cypress.trashAssetsBeforeRuns,
      viewportWidth: config.base.cypress.viewportWidth,
      viewportHeight: config.base.cypress.viewportHeight,
      defaultCommandTimeout: config.base.cypress.defaultCommandTimeout,
      requestTimeout: config.base.cypress.defaultCommandTimeout,
      watchForFileChanges: config.base.cypress.watchForFileChanges,
      pageLoadTimeout: config.base.cypress.pageLoadTimeout,
      browser: config.base.cypress.browser,
      projectId: config.base.cypress.projectId,
      retries: 0,
      screenshotsFolder: 'logs/screenshots',
      videosFolder: 'logs/videos',
    },
    path.join(system.cyRunnerPath(), 'cypress.json')
  )
}

// Deal with stop on fail
exports.stopOnFail = async (config, step, runUrl) => {
  logger.msgSection(`Stop on fail triggered by strategy ${step.strategy}`)

  step.specsPassed.sort()
  step.specsPassed.forEach((spec) => {
    logger.msgOk(this.specNameClean(spec))
  })

  logger.newLine()

  step.specsFailed.sort()
  step.specsFailed.forEach((spec) => {
    logger.msgError(this.specNameClean(spec))
  })

  await workspace.teardown(config)
  if (runUrl != null) this.showDashboard(runUrl)
  system.crash('Triggered stop on fail', step.strategy)
}

// Show URL for Cypress Dashboard or Sorry Cypress
exports.showDashboard = (url) => {
  logger.msgSection(`[Cypress Dashboard](${url})`, true)
  logger.msgOk(url, true)
}

// Beauty spec name to show on logs
exports.specNameClean = (spec) => {
  const clean = spec.replace('cypress/integration/', '')

  return clean.replace('cypress-shared/integration/', '')
}

// Open Cypress
exports.open = async () => {
  logger.msgSection('Running in dev mode')

  const CY_FOLDER = this.getCypressFolder()
  const options = {
    config: {
      integrationFolder: `${CY_FOLDER}/integration`,
      supportFile: `${CY_FOLDER}/support`,
      fixturesFolder: `${CY_FOLDER}/fixtures`,
      pluginsFile: `${CY_FOLDER}/plugins/index.js`,
      downloadsFolder: `${CY_FOLDER}/downloads`,
    },
  }

  logger.msgWarn('Verity if the workspace has the Apps you need already')

  try {
    await cypress.open(options)
  } catch (e) {
    system.crash('Failed to open Cypress', e.message)
  }
}

// Run Cypress
exports.run = async (test, config, addOptions = {}) => {
  // Mix Cypress base folder isn't allowed
  const SPEC_PATH = path.parse(test.specs[0]).dir
  const CY_FOLDER = this.getCypressFolder()

  // Build options
  let options = {
    config: {
      integrationFolder: SPEC_PATH,
      supportFile: `${SPEC_PATH.split(path.sep)[0]}/support`,
      fixturesFolder: `${CY_FOLDER}/fixtures`,
      pluginsFile: `${CY_FOLDER}/plugins/index.js`,
      downloadsFolder: `${CY_FOLDER}/downloads`,
    },
    spec: test.specs,
    headed: config.base.cypress.runHeaded,
    browser: config.base.cypress.browser,
    quiet: config.base.cypress.quiet,
  }

  const testToRun = []
  let maxJobs = 1

  // maxJobs = 0 --> running local without Sorry Cypress
  if (config.base.cypress.maxJobs) {
    options = tuneOptions(options, config)
    merge(options, addOptions)
    maxJobs = getMaxJobs(test, config)
  }

  // Mount parallel jobs
  for (let i = 0; i < maxJobs; i++) {
    testToRun.push(
      // eslint-disable-next-line no-loop-func
      cypress.run(options).then((result) => {
        const specsPassed = []
        const specsFailed = []

        // If Cypress fails, roll back the spec
        if (result.failures) {
          logger.msgError('Got error from Cypress')
          logger.msgPad(JSON.stringify(result))

          return { success: false, specsPassed, specsFailed, runUrl: null }
        }

        // Remove sensitive data and get runUrl
        delete result.config
        const { runUrl } = result

        // Return specs passed and failed
        result.runs.forEach((run) => {
          const logName = run.spec.name.replace('.js', '-result.yaml')
          const logFile = path.join(logger.logPath(), logName)
          const logFlow = {}

          logFlow[`epoc-${system.tick()}`] = result
          storage.append(jsYaml.dump(logFlow), logFile)

          if (run.stats.failures) specsFailed.push(run.spec.relative)
          else specsPassed.push(run.spec.relative)
        })

        return { success: true, specsPassed, specsFailed, runUrl }
      })
    )
  }

  try {
    return await Promise.all(testToRun)
  } catch (e) {
    await workspace.teardown(config)
    system.crash('Failed to run Cypress', e)
  }
}

// Tune options
function tuneOptions(options, config) {
  const RUN_ID = process.env.GITHUB_RUN_ID ?? system.getId()
  const RUN_ATTEMPT = process.env.GITHUB_RUN_ATTEMPT ?? 1

  options.key = config.base.cypress.dashboardKey
  options.record = true
  options.ciBuildId = `${RUN_ID}-${RUN_ATTEMPT}`

  // Configure Cypress to use Sorry Cypress if not in CI
  if (!system.isCI()) process.env.CYPRESS_INTERNAL_ENV = 'development'

  return options
}

// Calculate max jobs
function getMaxJobs(test, config) {
  let maxJobs = 1

  if (test.parallel) {
    maxJobs =
      test.specs.length < config.base.cypress.maxJobs
        ? test.specs.length
        : config.base.cypress.maxJobs
  }

  return maxJobs
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of cy-runner/node/wipe.js from the cy-runner VTEX app.

[Assistant]
Here's the implementation:

```javascript
const logger = require('./logger')
const cypress = require('./cypress')
const test = require('./test')

module.exports.wipe = async (config) => {
  const { wipe } = config.workspace

  // eslint-disable-next-line vtex/prefer-early-return
  if (wipe.enabled) {
    logger.msgOk('Wiping data')

    // Disable parallelism for wipe and increase verbosity
    logger.msgPad('Disabling parallelism')
    config.base.cypress.maxJobs = 0

    // Remove data
    logger.msgPad('Running wipe')
    const xvfb = await test.startXvfb()
    const results = await cypress.run(wipe, config)

    await test.stopXvfb(xvfb)
    let success = false

    results.forEach((result) => {
      if (result.success) if (!result.specsFailed?.length) success = true
    })

    success
      ? logger.msgOk('Data wiped successfully')
      : logger.msgError('Failed to wipe data')
  }
}

```


[System] You are an expert VTEX IO developer. You specialize in building VTEX Store Framework apps, React components, GraphQL APIs, and Node.js services for e-commerce platforms.

[User] Show me the implementation of cy-runner/node/jira.js from the cy-runner VTEX app.

[Assistant]
Here's the implementation:

```javascript
const axios = require('axios')

const logger = require('./logger')
const system = require('./system')

module.exports.issue = async (config, specsFailed, runUrl) => {
  logger.msgSection('Jira ticket automation', true)

  // If LOCAL, avoid any ticket creation
  if (!system.isCI()) {
    logger.msgWarn('Not on CI, skipping ticket creation')

    return
  }

  const {
    GITHUB_REPOSITORY,
    GITHUB_REF,
    GITHUB_RUN_ID,
    GITHUB_SERVER_URL,
    GITHUB_ACTOR,
  } = process.env

  const JIRA = config.base.jira
  const [, , GH_REF] = GITHUB_REF.split('/')
  const PR_URL = `${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/pull/${GH_REF}`
  const CY_URL = `https://dashboard.cypress.io/projects/${config.base.cypress.projectId}/runs`
  const RUN_URL = `${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}`
  const IS_SCH = process.env.GITHUB_EVENT_NAME === 'schedule' ?? false
  const IS_DIS = process.env.GITHUB_EVENT_NAME === 'workflow_dispatch' ?? false
  const IS_PRN = /^-?\d+$/.test(`${GH_REF}`)

  // If DISPATCH, avoid any ticket creation
  if (IS_DIS) {
    logger.msgWarn('It was triggered by dispatch, skipping ticket creation')

    return
  }

  // Jira - You can set config.base.jira.testing as true for tests
  // JIRA.board = JIRA.testing || IS_SCH ? 'ENGINEERS' : JIRA.board
  JIRA.board = 'ENGINEERS'
  const PR = IS_PRN ? `PR #${GH_REF}` : 'FORK'
  const SUMMARY = IS_SCH ? `SCHEDULE ${GITHUB_REPOSITORY}:` : `${PR}:`
  const JQL = `summary ~ '${SUMMARY}' AND project = '${JIRA.board}' AND statusCategory IN ('undefined', 'In Progress', 'To Do')`
  const PRIORITY = JIRA.priority ?? 'High'
  const JIRA_KEY = await searchIssue(JIRA.account, JIRA.authorization, JQL)

  // Prepare failures with links
  const FAILURES = []
  let MARKS = []

  if (typeof runUrl !== 'undefined') {
    MARKS = [
      {
        type: 'link',
        attrs: {
          href: `${runUrl}/test-results?statuses=[{"value":"FAILED","label":"FAILED"}]`,
        },
      },
    ]
  }

  specsFailed.forEach((fail) => {
    FAILURES.push({
      type: 'text',
      text: `\n==> ${fail}`,
      marks: MARKS,
    })
  })

  // Payload for ticket creation
  const ADD_ISSUE = JSON.stringify({
    fields: {
      project: {
        key: JIRA.board,
      },
      summary: `${SUMMARY} E2E test failed for ${GITHUB_REPOSITORY}`,
      description: {
        type: 'doc',
        version: 1,
        content: [
          {
            type: 'paragraph',
            content: [
              {
                type: 'text',
                text: 'E2E test ',
              },
              {
                type: 'text',
                text: `${SUMMARY}`,
                marks: [
                  {
                    type: 'link',
                    attrs: {
                      href: PR_URL,
                    },
                  },
                ],
              },
              {
                type: 'text',
                text: ` ${
                  IS_PRN ? 'created' : 'authorized'
                } by ${GITHUB_ACTOR}; failed on\n`,
              },
              ...FAILURES,
            ],
          },
          {
            type: 'paragraph',
            content: [
              {
                type: 'text',
                text: '\nTo get more information about the errors, please take a look at ',
              },
              {
                type: 'text',
                text: `artifacts on GitHub Action`,
                marks: [
                  {
                    type: 'link',
                    attrs: {
                      href: RUN_URL,
                    },
                  },
                ],
              },
              {
                type: 'text',
                text: ' or ',
              },
              {
                type: 'text',
                text: `Cypress Dashboard`,
                marks: [
                  {
                    type: 'link',
                    attrs: {
                      href: CY_URL,
                    },
                  },
                ],
              },
              {
                type: 'text',
                text: '.',
              },
            ],
          },
        ],
      },
      issuetype: {
        name: JIRA.issueType,
      },
      // Priority - Lowest, Low, Medium, High, Highest
      priority: {
        name: PRIORITY,
      },
      // Bug priority - Must Fix, Should Fix, Unbreak Now
      customfield_10115: {
        value: 'Should Fix',
      },
    },
    update: {},
  })

  const CFG_ADD_ISSUE = {
    method: 'post',
    url: `https://${JIRA.account}.atlassian.net/rest/api/3/issue`,
    headers: {
      Authorization: `Basic ${JIRA.authorization}`,
      'Content-Type': 'application/json',
    },
    params: {
      updateHistory: true,
      applyDefaultValues: false,
    },
    data: ADD_ISSUE,
  }

  // Payload for ticket update
  const UPDATE_ISSUE = JSON.stringify({
    body: {
      version: 1,
      type: 'doc',
      content: [
        {
          type: 'paragraph',
          content: [
            {
              type: 'text',
              text: 'E2E test for ',
            },
            {
              type: 'text',
              text: `${SUMMARY} ${GH_REF}`,
              marks: [
                {
                  type: 'link',
                  attrs: {
                    href: PR_URL,
                  },
                },
              ],
            },
            {
              type: 'text',
              text: ` failed again on:`,
            },
            ...FAILURES,
          ],
        },
      ],
    },
    visibility: null,
  })

  const CFG_UPDATE_ISSUE = {
    method: 'post',
    url: `https://${JIRA.account}.atlassian.net/rest/api/3/issue/${JIRA_KEY}/comment`,
    headers: {
      Authorization: `Basic ${JIRA.authorization}`,
      'Content-Type': 'application/json',
    },
    data: UPDATE_ISSUE,
  }

  // Update or create the issue
  let CFG = null
  let MSG = null
  let KEY = null

  switch (JIRA_KEY) {
    case 'abort':
      logger.msgError('Error on the JQL')
      logger.msgPad(JQL)

      return

    case 'create':
      CFG = CFG_ADD_ISSUE
      MSG = 'create'
      break

    default:
      CFG = CFG_UPDATE_ISSUE
      MSG = 'update'
  }

  await axios(CFG)
    .then((response) => {
      const { key: ISSUE_KEY } = response.data

      KEY = typeof ISSUE_KEY === 'undefined' ? JIRA_KEY : ISSUE_KEY
      const URL = `https://${JIRA.account}.atlassian.net/browse/${KEY}`

      logger.msgOk(`Issue ${KEY} ${MSG}d`, true)
      logger.msgPad(URL, true)
    })
    .catch((e) => {
      logger.msgError(`Failed to ${MSG} issue`, true)
      logger.msgPad(e, true)
    })
}

// Search issues on Jira
async function searchIssue(account, authorization, jiraJQL) {
  let key = null

  // Configuration
  const CFG = {
    method: 'get',
    url: `https://${account}.atlassian.net/rest/api/3/search`,
    headers: {
      Authorization: `Basic ${authorization}`,
      'Content-Type': 'application/json',
    },
    params: {
      jql: jiraJQL,
      startAt: 0,
      maxResults: 2,
    },
  }

  await axios(CFG)
    .then((response) => {
      if (response.data.total === 1) {
        key = response.data.issues[0].key
        logger.msgOk(`Opened issue ${key} found`)
      } else if (response.data.total > 1) {
        key = 'abort'
        logger.msgError('More than one issue found')
        logger.msgPad(jiraJQL)
      } else {
        key = 'create'
        logger.msgWarn('A new issue will be created')
      }
    })
    .catch((e) => {
      logger.msgError('Querying Jira API')
      logger.msgPad(e)
    })

  return key
}

```