This package contains components and other code (such as WindiCSS config) that is shared between the app
(Cypress web app) and launchpad
(Cypress Electron app) packages. Any functionality that is intended to be the same in both can be added here and imported in those packages as needed. Base components like form inputs, cards, and modals, are written here, as well as higher-level components that exist in both apps, like the header.
Conceivably, other packages may be created that also import from this shared component package.
In this package, we use Cypress Component Tests to develop the components in isolation, and no E2E tests. E2E tests should be written in the packages that consume these components (app
and launchpad
). This means that there is no app to visit for development, instead, we open Cypress:
## from repo root
yarn workspace @packages/frontend-shared cypress:open
For the best development experience, you will want to use VS Code with the Volar extension. This will give you type completion inside vue
files.
## from repo root
yarn workspace @packages/frontend-shared cypress:run:ct
WindiCSS can create an awesome interactive summary showing our usage of utility classes and design tokens. Running this command will generate this report and serve it on localhost.
## from this directory
yarn windi
This will be useful from time to time so that we can audit our usage of these classes and extract repeated patterns into Windi shortcuts or otherwise consolidate them, when it makes sense to do so.
There are two shared components involved with links - BaseLink
, and ExternalLink
. BaseLink
is responsible for default colors and hover/focus styles. ExternalLink
wraps BaseLink
is responsible for managing the GraphQL mutation that triggers links to open the in the user's default browser.
See the readme in the src/public/shiki/themes directory
These apply to this package, app
, and launchpad
, as well as any future work in this Vue-Tailwind-GQL stack. The goal is for this to provide useful context for new developers adding features to the codebase, or making changes to existing features. There are pros and cons to all of these decisions, but rather than get into those in detail, this is just a document of what practices we are following.
We recommend component-based test driven development. More details in the Testing Practices guide. To make changes to an existing component:
- Open Cypress and go to the spec that covers the component (often it's 1:1 but sometimes components are tested via their parents)
- Update the test to reflect the desired change (or part of it)
- Implement the change in the component
- Add Percy snapshot for any new unique states covered by the change
To create a new component:
- Add a component spec file and the component file itself as siblings in the desired location
- In the spec file, import and mount the component. If the component depends on a GQL fragment, use
mountFragment
to mount the component so it can receive test data through thegql
prop.
If you are new to Vue 3, there are some new features we are using in this codebase that you should become familiar with.
But first, if you are coming from React to Vue 3, here's a small potential gotcha to note as you read and write Vue code: the idea of a ref
in Vue is similar to a ref
in React but with a major difference. In React, when a ref's value changes, it doesn't trigger an update, or get "noticed" at all, by default. In Vue, a ref is part of the reactivity system and when the value updates, the component knows this and the updated value is reflected wherever the value is referenced. This can mean DOM updates, watchers firing, etc.
Here are some features of Vue 3, and packages in the ecosystem, that are worth knowing about as we work in the codebase.
We are using the Composition API and specifically the <script setup>
syntax in our Vue Single File Components (SFCs). This removes a lot of boilerplate, and because of that it's not always obvious reading the code which Vue features are being leveraged, compared to Vue 2.
If you are familiar with the Options API, which was the main way to write component script sections in Vue 2, the separation of variables and functions into named parts of the Options object like computed
and data
provided a familiar, but unwieldy, structure in each component. The Composition API lets us use those features anywhere we like, without dividing things into a predefined structure. <script setup>
is a way to write Composition API code with less boilerplate and some other advantages described in the docs.
TS-friendly, modularized state management - this is what we use instead of Vuex for the small amount of global state we have.
Broad collection of composable utilities that provides reactive values for various common events and DOM properties, CSS rules, local storage and a lot of other stuff. This library exposes many common low-level event listeners, exposing them to Vue and managing the necessary setup/teardown. It's like a front-end lodash, so where the VueUse implementation of something works for us, we prefer to use it instead of roll our own.
We are using some components from Headless UI as the basis for UI patterns like modals, custom dropdowns, and expanding panels. We use Headless UI because it is well documented and the accessibility features are properly thought out. These advantages outweigh the occasional workarounds we have to use in order to get sophisticated behavior working that Headless UI does not support.
Only @packages/app
has a router, so details are described in its README.
We use Tailwind through WindiCSS. The codebase is utility-driven and all CSS that can be achieved through utility classes is written that way. The main way to reuse CSS in multiple places is to extract a component that applies the utility classes and can wrap other elements as needed.
WindiCSS can create CSS classes as build time based on what class names we use in our components. That means syntax like this will work:
<p class="p-20px">
This allows us to specify explicit pixel values for measurements. We follow this pattern throughout the Cypress App codebase.
As an example: instead of using the class m-2
which applies the rule margin: 0.5rem
in Tailwind and usually creates a margin of 8px
(with 16px font size), we write the class as m-8px
, from which Windi will generate a class with the rule margin: 8px
.
Cy has a very custom icon library, to meet the following needs:
- Most of our icons are duo-tone
- They must be styled with different colors in different contexts
- Since they're duotone, you want to target the specific strokes and fills of the SVGs to color them
- We should be able to apply color styles to icons with the same WindiCSS approach we use for other styles - meaning we can write dynamic classes and use prefixes like
hover:
orgroup-focus:
to change the colors. - We don't want to import icons in Vue SFCs for basic use in templates, they should 'just work'.
To add a new icon:
- Export the icon from Figma (all icons should come from the design system in Figma) as an SVG.
- Add the SVG to the icons folder.
- Name the file following the existing convention in there:
icon-name_x[size in pixels]
, e.g.arrow-down_x16.svg
. - Manually edit the SVG file to add classes
icon-dark
andicon-light
to the dark and light internal elements, and save the file. If an icon path doesn't define a class, nothing bad will happen, it just won't get targeted by any styling.light
anddark
refer to the 2 main colors present in a duotone icon, since often there is a "light" and "dark" color in the design. These can be though of as primary and secondary color,color1
,color2
, or anything else. - Finally, you don't need to expose anything.
./src/assets/icons
is automatically watched and loaded 😮
Now the icon is ready to be used in Vue SFC templates like this:
/* This just works. No imports necessary */
<i-cy-path-to-icon_x16 />
This is possible through the use of the auto-importing feature of unplugin-icons, which is set up in vite.config.ts.
To use an icon in tests, or to refer to it in the <script>
block of a component, import it this way:
import MyIcon from '~icons/cy/path-to-icon_x16'
This example renders a book icon from ./src/assets/book_x16.svg
and makes it pink for the 'light' color and purple for the 'dark' color. It uses the hover:
pseudoclass to invert the light/dark colors on hover. A class string formatted like this:
icon-light
+ -any-color-100
will be used to target paths and strokes inside the SVG that have the class icon-light
and apply the specific color to those elements.
<i-cy-book_x16 class="
icon-light-pink-100
icon-dark-purple-500
hover:icon-light-purple-500
hover:icon-dark-pink-100
" />
To support selecting specific paths while keeping Tailwind's incredibly helpful interaction helpers (e.g. group-hover
or group-focus
), we use a WindiCSS plugin. Windi configuration lives in the windi.config.ts file in this package.
We consider accessibility a core part of front-end code quality. When possible, components should be built out using standard semantic HTML elements. If there are no plain HTML solutions for a particular interaction, we can reach for a library (like HeadlessUI above) that implements known patterns in an accessible way. In rare cases we will augment our HTML with ARIA roles. Tests should use the accessible name or label for interactive elements as described in the testing guide.
The Accessibility Tree available in your browser's dev tools will show how the nature, structure, and labelling of the elements in the DOM is presented to assistive technology. We can use this to explore the accessibility of our components, especially when creating or reviewing more complex UI interactions, to make sure the content that appears there makes sense.
GraphQL is the main source of data and state from the server in the app and launchpad. In their <script>
block, Vue components describe the data they will receive in Queries or Fragments, as well as the Mutations they will trigger, which can set data on the server or trigger side effects like launching a project and opening the the user's browser.
Our GraphQL frontend client is urql using the urql-vue package. This provides composables like useQuery
and useMutation
to simplify interacting with GraphQL.
By convention, we use a prop named gql
to represent the source of data, so it is common to see things like props.gql.currentProject
.
In the long run, files in the src/components
directory are intended as the foundation of a design system. As such they may be used in many contexts other than the Cypress App and Launchpad. There are some components that are only intended to be shared between App and Launchpad and make use of GraphQL queries and mutations. These will only work correctly if placed within src/gql-components
directory, because only that directory is specified in graphql-codegen.yml. This is intended to maintain the separation between genuinely reusable components driven by props and events, and gql-driven components that are tightly bound to the implementation of App and Launchpad.
Some components need spec JSON fixtures to test them. generate-stub-specs
script helps you generate them.
# inside frontend-shared project
yarn generate-stub-specs <filename> <n> <baseTypeName> [--app]
filename
is the name of the file you want to create.n
is the number of stub specs to be created in the file.baseTypeName
isSpec
orFileParts
.--app
option creates fixtures inside theapp
package. Without it, they're added inside thefrontend-shared
package.