From 9b9c58eba9a5492e4c885ff94258fa360ffb19ba Mon Sep 17 00:00:00 2001 From: domyen Date: Wed, 29 Nov 2023 15:19:19 -0500 Subject: [PATCH 1/7] Update branch and baseline docs https://linear.app/chromaui/issue/DX-789/docs-simplify-branches-and-baselines-page --- src/content/ci/azure-pipelines.md | 2 +- src/content/ci/bitbucket-pipelines.md | 4 +- src/content/ci/circleci.md | 2 +- src/content/ci/custom-ci-provider.md | 2 +- src/content/ci/github-actions.md | 2 +- src/content/ci/gitlab.md | 2 +- src/content/ci/jenkins.md | 2 +- src/content/ci/travisci.md | 2 +- src/content/getStarted/test.md | 2 +- src/content/getStarted/workflow.md | 4 +- .../snapshot/branching-and-baselines.md | 103 +++++++++++------- src/content/snapshot/turbosnap.mdx | 27 +++-- 12 files changed, 94 insertions(+), 60 deletions(-) diff --git a/src/content/ci/azure-pipelines.md b/src/content/ci/azure-pipelines.md index 75285647..b554a1b7 100644 --- a/src/content/ci/azure-pipelines.md +++ b/src/content/ci/azure-pipelines.md @@ -311,7 +311,7 @@ If the builds are a result of direct commits to `main`, you will need to accept #### Azure squash/rebase merge and the "main" branch -Azure's squash/rebase merge functionality creates new commits that have no association to the branch being merged. If you are already using this option, then we will automatically detect this situation and bring baselines over (see [Branching and Baselines](/docs/branching-and-baselines#squash-and-rebase-merging) for more details). +Azure's squash/rebase merge functionality creates new commits that have no association to the branch being merged. If you are already using this option, then we will automatically detect this situation and bring baselines over (see [Branching and Baselines](/docs/branching-and-baselines#how-do-baselines-get-preserved-during-squash-and-rebase-merging) for more details). If you’re using this functionality but notice the incoming changes were not accepted as baselines in Chromatic, then you'll need to adjust the pipeline and include the `--auto-accept-changes` flag. For example: diff --git a/src/content/ci/bitbucket-pipelines.md b/src/content/ci/bitbucket-pipelines.md index 6c85d8d0..11db58f5 100644 --- a/src/content/ci/bitbucket-pipelines.md +++ b/src/content/ci/bitbucket-pipelines.md @@ -232,7 +232,7 @@ If the builds are a result of direct commits to `main`, you will need to accept #### BitBucket squash/rebase merge and the "main" branch -BitBucket's squash/rebase merge functionality creates new commits that have no association to the branch being merged. If you are already using this option, then we will automatically detect this situation and bring baselines over (see [Branching and Baselines](/docs/branching-and-baselines#squash-and-rebase-merging) for more details). +BitBucket's squash/rebase merge functionality creates new commits that have no association to the branch being merged. If you are already using this option, then we will automatically detect this situation and bring baselines over (see [Branching and Baselines](/docs/branching-and-baselines#how-do-baselines-get-preserved-during-squash-and-rebase-merging) for more details). If you’re using this functionality but notice the incoming changes were not accepted as baselines in Chromatic, then you'll need to adjust the pipeline and include the `--auto-accept-changes` flag. For example: @@ -296,7 +296,7 @@ Including the `--ignore-last-build-on-branch` flag ensures the latest build for #### BitBucket pipelines and patch builds -If you're creating a [patch build](/docs/branching-and-baselines#patch-builds) in Chromatic to fix a missing pull request comparison, you'll need to adjust your existing pipeline to the following: +If you're creating a [patch build](/docs/branching-and-baselines#what-happens-when-the-merge-base-build-isnt-found-patch-builds) in Chromatic to fix a missing pull request comparison, you'll need to adjust your existing pipeline to the following: ```yml # bitbucket-pipelines.yml diff --git a/src/content/ci/circleci.md b/src/content/ci/circleci.md index 4e5d8507..beba1613 100644 --- a/src/content/ci/circleci.md +++ b/src/content/ci/circleci.md @@ -234,7 +234,7 @@ If the builds are a result of direct commits to `main`, you will need to accept #### Squash/rebase merge and the "main" branch -We use GitHub, GitLab, and Bitbucket APIs respectively to detect squashing and rebasing so your baselines match your expectations no matter your Git workflow (see [Branching and Baselines](/docs/branching-and-baselines#squash-and-rebase-merging) for more details). +We use GitHub, GitLab, and Bitbucket APIs respectively to detect squashing and rebasing so your baselines match your expectations no matter your Git workflow (see [Branching and Baselines](/docs/branching-and-baselines#how-do-baselines-get-preserved-during-squash-and-rebase-merging) for more details). If you’re using this functionality but notice the incoming changes were not accepted as baselines in Chromatic, then you'll need to adjust the `chromatic` command and include the `--auto-accept-changes` flag. For example: diff --git a/src/content/ci/custom-ci-provider.md b/src/content/ci/custom-ci-provider.md index ff06e870..3787d487 100644 --- a/src/content/ci/custom-ci-provider.md +++ b/src/content/ci/custom-ci-provider.md @@ -161,7 +161,7 @@ If the builds are a result of direct commits to `main`, you will need to accept #### Squash/rebase merge and the "main" branch -We use GitHub, GitLab, and Bitbucket APIs respectively to detect squashing and rebasing so your baselines match your expectations no matter your Git workflow (see [Branching and Baselines](/docs/branching-and-baselines#squash-and-rebase-merging) for more details). +We use GitHub, GitLab, and Bitbucket APIs respectively to detect squashing and rebasing so your baselines match your expectations no matter your Git workflow (see [Branching and Baselines](/docs/branching-and-baselines#how-do-baselines-get-preserved-during-squash-and-rebase-merging) for more details). If you’re using this functionality but notice the incoming changes were not accepted as baselines in Chromatic, then you'll need to adjust the `chromatic` command and include the `--auto-accept-changes` flag. For example: diff --git a/src/content/ci/github-actions.md b/src/content/ci/github-actions.md index ac2b168d..d29b27c4 100644 --- a/src/content/ci/github-actions.md +++ b/src/content/ci/github-actions.md @@ -535,7 +535,7 @@ If the builds result from direct commits to `main`, you must accept changes to k #### GitHub squash/rebase merge and the "main" branch -GitHub's squash/rebase merge functionality creates new commits that have no association with the branch being merged. If you've enabled our GitHub application in the [UI Review](/docs/review) workflow, then we will automatically detect this situation and bring baselines over (see [Branching and Baselines](/docs/branching-and-baselines#squash-and-rebase-merging) for more details). +GitHub's squash/rebase merge functionality creates new commits that have no association with the branch being merged. If you've enabled our GitHub application in the [UI Review](/docs/review) workflow, then we will automatically detect this situation and bring baselines over (see [Branching and Baselines](/docs/branching-and-baselines#how-do-baselines-get-preserved-during-squash-and-rebase-merging) for more details). If you’re using this functionality but notice the incoming changes were not accepted as baselines in Chromatic, then you'll need to adjust the workflow to include a new step with the `autoAcceptChanges` option. For example: diff --git a/src/content/ci/gitlab.md b/src/content/ci/gitlab.md index 96e67361..331bac8b 100644 --- a/src/content/ci/gitlab.md +++ b/src/content/ci/gitlab.md @@ -248,7 +248,7 @@ If the builds are a result of direct commits to `main`, you will need to accept #### GitLab squash/rebase merge and the "main" branch -GitLab's squash/rebase merge functionality creates new commits that have no association to the branch being merged. If you are already using this option, then we will automatically detect this situation and bring baselines over (see [Branching and Baselines](/docs/branching-and-baselines#squash-and-rebase-merging) for more details). +GitLab's squash/rebase merge functionality creates new commits that have no association to the branch being merged. If you are already using this option, then we will automatically detect this situation and bring baselines over (see [Branching and Baselines](/docs/branching-and-baselines#how-do-baselines-get-preserved-during-squash-and-rebase-merging) for more details). If you’re using this functionality but notice the incoming changes were not accepted as baselines in Chromatic, then you'll need to adjust the pipeline and include the `--auto-accept-changes` flag. For example: diff --git a/src/content/ci/jenkins.md b/src/content/ci/jenkins.md index 7cb3a7d3..56016d70 100644 --- a/src/content/ci/jenkins.md +++ b/src/content/ci/jenkins.md @@ -317,7 +317,7 @@ If the builds are a result of direct commits to `main`, you will need to accept #### Squash/rebase merge and the "main" branch -We use GitHub, GitLab, and Bitbucket APIs respectively to detect squashing and rebasing so your baselines match your expectations no matter your Git workflow (see [Branching and Baselines](/docs/branching-and-baselines#squash-and-rebase-merging) for more details). +We use GitHub, GitLab, and Bitbucket APIs respectively to detect squashing and rebasing so your baselines match your expectations no matter your Git workflow (see [Branching and Baselines](/docs/branching-and-baselines#how-do-baselines-get-preserved-during-squash-and-rebase-merging) for more details). If you’re using this functionality but notice the incoming changes were not accepted as baselines in Chromatic, then you'll need to adjust the pipeline and include a new Chromatic stage using the `--auto-accept-changes` flag. For example: diff --git a/src/content/ci/travisci.md b/src/content/ci/travisci.md index ff48ea6c..3a720a21 100644 --- a/src/content/ci/travisci.md +++ b/src/content/ci/travisci.md @@ -221,7 +221,7 @@ If the builds are a result of direct commits to `main`, you will need to accept #### Squash/rebase merge and the "main" branch -We use GitHub, GitLab, and Bitbucket APIs respectively to detect squashing and rebasing so your baselines match your expectations no matter your Git workflow (see [Branching and Baselines](/docs/branching-and-baselines#squash-and-rebase-merging) for more details). +We use GitHub, GitLab, and Bitbucket APIs respectively to detect squashing and rebasing so your baselines match your expectations no matter your Git workflow (see [Branching and Baselines](/docs/branching-and-baselines#how-do-baselines-get-preserved-during-squash-and-rebase-merging) for more details). If you’re using this functionality but notice the incoming changes were not accepted as baselines in Chromatic, then you'll need to adjust the workflow and include a new Chromatic job with the `--auto-accept-changes` flag. For example: diff --git a/src/content/getStarted/test.md b/src/content/getStarted/test.md index 9fe54423..86571ce9 100644 --- a/src/content/getStarted/test.md +++ b/src/content/getStarted/test.md @@ -7,7 +7,7 @@ sidebar: { order: 3, label: "UI Tests" } # UI Tests -UI tests pinpoint visual changes and verify user [interactions](/docs/interactions). They capture a [snapshot](/docs/snapshots) of every story in a cloud browser environment. Whenever you push code, Chromatic generates a new set of snapshots and compares them against [baseline snapshots](/docs/branching-and-baselines#baselines). If there are changes, you verify that they're intentional. If there are test errors, you get notified to fix them. +UI tests pinpoint visual changes and verify user [interactions](/docs/interactions). They capture a [snapshot](/docs/snapshots) of every story in a cloud browser environment. Whenever you push code, Chromatic generates a new set of snapshots and compares them against [baseline snapshots](/docs/branching-and-baselines#whats-a-baseline). If there are changes, you verify that they're intentional. If there are test errors, you get notified to fix them. ![UI test](../../images/workflow-uitest.png) diff --git a/src/content/getStarted/workflow.md b/src/content/getStarted/workflow.md index d4ddb244..02e1b0ef 100644 --- a/src/content/getStarted/workflow.md +++ b/src/content/getStarted/workflow.md @@ -33,7 +33,7 @@ We recommend running Chromatic on every push. This ensures that Chromatic is reg Each snapshot is associated with a commit. That enables you to pinpoint the particular commit where a change was introduced. -It also allows you to [visualize baseline history](/docs/branching-and-baselines#visualize-baseline-history). You can review the commits to see how a component changes over time. +It also allows you to [visualize baseline history](/docs/branching-and-baselines#how-do-i-visualize-baseline-history-for-a-story). You can review the commits to see how a component changes over time. Not running Chromatic on every commit makes it harder to review diffs and increases the risk of missing changes. @@ -84,7 +84,7 @@ To debug, you can launch the published Storybook to reproduce the exact state of -UI Tests are similar to other types of testing (unit, E2E, etc.), in that they enable developers to catch and fix regressions. UI Tests compare the snapshot of a story with the previously accepted [baseline](/docs/branching-and-baselines#branches-and-baselines) in your git history (typically on the same branch). If there are changes, you'll get a diff of the changes. If the changes are intentional, press the accept button to update the baselines. +UI Tests are similar to other types of testing (unit, E2E, etc.), in that they enable developers to catch and fix regressions. UI Tests compare the snapshot of a story with the previously accepted [baseline](/docs/branching-and-baselines#whats-a-baseline) in your git history (typically on the same branch). If there are changes, you'll get a diff of the changes. If the changes are intentional, press the accept button to update the baselines. Once all changes are approved, the UI is considered “ready” and you can move to the UI Review phase. diff --git a/src/content/snapshot/branching-and-baselines.md b/src/content/snapshot/branching-and-baselines.md index 8c0efe0b..df1a92e9 100644 --- a/src/content/snapshot/branching-and-baselines.md +++ b/src/content/snapshot/branching-and-baselines.md @@ -1,49 +1,49 @@ --- layout: "../../layouts/Layout.astro" title: Branches and baselines -description: Learn how Chromatic uses git information to tie your builds together +description: How Chromatic decides what snapshots to compare when using UI Tests and UI Review sidebar: { order: 3 } --- -# Branches and baselines +# Branches, baselines, and git history -Chromatic uses your git history to decide how to check stories for changes for both [UI Tests](/docs/test) and [UI Review](/docs/review). The way it works is intended to get out of your way and do what you expect. However, there can be situations where things get confusing; this document describes in detail the way Chromatic does it. +This document breaks down how Chromatic decides what snapshots to compare when using UI Tests or UI Review. -### UI Review: comparing branches +- [UI Tests](#ui-tests-verify-changes-on-one-branch): Verify changes on one branch (uses baselines) +- [UI Review](#ui-review-compare-two-branches-for-changes): Compare two branches for changes (does not use baselines) -For [UI Review](/docs/review), Chromatic's aim is to show you what will change on the base branch when you merge this PR. +## UI Tests: Verify changes on one branch -As such, Chromatic will compare each story on the head branch with the way the story looked on the base branch when you branched off or last merged-in. This is similar to what systems like GitHub do when showing you the code changes in a PR. +UI Tests serve as visual regression tests between builds, similar to snapshot testing. When modifications are made in a PR, the tests fail until the new snapshots are accepted as baselines. -Technically, to achieve that, we need to find the "merge-base build" to compare with. The method to do so is [explained below](#how-the-merge-base-is-calculated). +### What's a baseline? -### UI Tests: tracking baselines +A baseline is the last known “good” state of the story in a given [mode](/docs/modes). [UI Tests](/docs/test) take snapshots and compare them to baselines to detect changes. -For [UI Tests](/docs/test), we aim to keep an up to date "baseline" for each story (at a given viewport) that lives alongside the git history. One way to think about it is as if we checked in a snapshot file into your repository every time you accept a change (we don't do this but we aim to behave as if we did). +Chromatic's objective is to maintain an up to date "baseline" for each story. Baselines live alongside the git history and persist through git branching and merging. -That means once a snapshot is accepted as a baseline, it won’t need to be re-accepted until it changes, even through git branching and merging. The mechanism to achieve this is explained below. +Baselines only update when changes are [accepted](/docs/test#verify-ui-changes) by you or your team. Since baselines are tracked independently for each branch, when you merge that branch into another (for instance back into `main`), the baseline comes with it. -![Baselines](../../images/baselines.jpg) - -## Baselines - -Chromatic's [UI Tests](/docs/test) compare snapshots to a baseline: the last known “good” state of the story. Each story has its own baseline that is tracked independently on each branch. +Conceptually, this is akin to storing a snapshot file in your repository with each accepted change, though Chromatic does not actually do this. -When you accept a snapshot you also update the baseline for that story on that branch. When you merge that branch into another (for instance back into `main`), the baseline comes with it. [Learn how we calculate baselines »](#how-baselines-are-calculated) +![Baselines](../../images/baselines.jpg) -## Branches +
+What happens to baselines when there are multiple branches? -Chromatic uses the branch that is checked out when you run a build to mark builds in our system. This means it is easy to see which builds belong to which branch of development, which components exist and are tested in which branch, and how a component has changed over the history of a branch. +When you are developing in a branch, the baseline snapshot is chosen from previous commits on the branch. This means if your team is developing on multiple branches in parallel, changes to the approved component snapshots on one branch will not affect the others. -#### Multiple branches +
-When you are developing in a branch, it is natural that the baseline image should be chosen from previous commits on the branch. This means if your team is developing on multiple branches in parallel, changes to the approved component screenshots on one branch will not affect the others. +
+What happens to baselines on merge? -#### Merging +When you merge two branches together, Chromatic can sometimes have two (or more) potential snapshots to use as the baseline (one from each branch). In such situations, Chromatic will choose the _most recent approved change_ as the baseline. -When you merge two branches together, Chromatic can sometimes have two (or more) potential screenshots to use as the baseline (one from each branch). In such situations, Chromatic will choose the _most recent approved change_ as the baseline. +
-#### Rebasing +
+What happens to baselines when a branch is rebased? If you rebase a branch (say updating to branch off the latest commit off `main`), then you create a new commit that isn't a git descendent of the previous (pre-rebase) commit on that branch. Conceptually, that might mean that Chromatic should throw away any approvals that were made for builds on the branch, however this is probably not what you want. @@ -59,7 +59,10 @@ Read our CI [documentation](/docs/ci). -#### Squash and rebase-merging +
+ +
+How do baselines get preserved during squash and rebase-merging? Chromatic detects squash and rebase merges. Your baselines are preserved between branches, even when squashing or rebasing wipes the Git history. @@ -71,19 +74,13 @@ This means Chromatic has no way to tell, using Git, that baselines accepted duri If you are using GitHub, you need to enable our GitHub App (on the [Pull Request](/docs/review) screen) for this feature to work. Bitbucket and GitLab will work out of the box. -## How baselines are calculated +
-As stated above, Chromatic maintains an individual baseline for each _story_, at each _viewport_, for each _commit_. That means as you make changes to your components, either by committing new code, merging other branches or otherwise, your baselines will follow your stories. +### How are baselines calculated? -The only way that baselines change is when you or someone in your team [accepts a change](/docs/test#verify-ui-changes). Usually what this means is that the baselines are what you’d expect as you work through a feature. +In Chromatic, a build contains of a set of snapshots, each of which is a snapshot of a single story in a single mode. The baseline is the last accepted snapshot on a given branch. Each branch has builds associated with it, so to find the baseline, we need to traverse git history for that branch to find the "ancestor" build. -However, sometimes the choice of baseline can be confusing. Let’s dig in a little further on how it works. - -#### Calculating the ancestor build(s) - -In Chromatic, a build contains of a set of snapshots, each of which is a screenshot of a single story at a single viewport. - -> If you are using multiple browsers, there may be more than one screenshot within a single snapshot, however as baselines are calculated above the level of the browser (all browsers share the same acceptance/denial), we can ignore multiple browsers when thinking about baselines. +#### Find the ancestor build(s) When you create a new build for a new commit, Chromatic will calculate a baseline for each snapshot in the build (unless the snapshot is for a new story). The first step to do that is to calculate the ancestor(s) for the build itself. @@ -121,7 +118,7 @@ You can see the ancestor builds listed on the build page: ![Ancestor Builds](../../images/ancestor-builds.png) -#### Calculating a snapshot baseline from the ancestor build(s) +#### Calculate a snapshot baseline from the ancestor build(s) Once we’ve got the ancestor builds for a build, the algorithm to calculate the baseline for any given snapshot goes like this: @@ -146,11 +143,22 @@ z - Build N+2 Suppose then in commit y, we changed the color of our submit buttons to be orange rather than green. However, we realized this was a mistake and denied the change. Then in commit z we changed the colour back to green. Then in Build N+2, we should compare the “new” green buttons to the original green buttons (from Build N). If they are back to the way they were before, the build should pass without you needing to intervene. If the green color is a different shade, Chromatic should show you a diff and you can decide if that’s what you wanted. -#### Multiple ancestor builds +
+How do browsers work with baselines? + +Baselines are calculated above the level of browsers. Clicking "accept" on a baseline will accept all snapshots taken in different browsers associated with that baseline. We can ignore multiple browsers when thinking about baselines. + +
+ +
+What if there are multiple ancestor builds? In the case that there are multiple ancestor builds, the algorithm to calculate the baseline is more or less the same. We can (potentially) end up with more than one baseline snapshot to use. To break ties, we assume that the most recently accepted snapshot is the one you want to compare to. -#### Visualize baseline history +
+ +
+How do I visualize baseline history for a story? When you [verify UI Test changes on Chromatic](/docs/test#verify-ui-changes), you'll see a historical set of baselines that correspond to the algorithm above. This helps you understand when the baseline changed, by who, and in which commit. @@ -160,9 +168,21 @@ When you [verify UI Test changes on Chromatic](/docs/test#verify-ui-changes), yo The snapshot marked "Most recent build...." is a change that hasn't been accepted as a baseline yet. The baseline marked "current baseline" is the last known good version of the snapshot that was accepted by Tom Coleman. Going back in the timeline, the listed baselines show previous times the component changed. -## How the merge base is calculated +
+ +## UI Review: Compare two branches for changes + +Similar to a code review, UI Review shows what changes will occur on the base branch upon PR merge, minimizing unintentional or disruptive merges. Chromatic compares each story on the head branch with its appearance on the base branch at the time of branching off (in git this is called the `merge base`). + +### How changesets are generated -To find the merge base build in Chromatic, it needs to track back from the current latest build on the PR until it finds a build that was on the base branch. Chromatic tracks back via the [ancestor builds](#calculating-the-ancestor-builds) of each build, which corresponds to the git commit history (keep in mind you may not have run a build for every single commit!). +UI Review generates changesets using the most recent snapshots on a build compared to the `merge base`. It does not use baselines to generate changesets so the changeset will show snapshots that are unreviewed (havent been accepted). + +Conceptually, this is similar to what systems like GitHub do when showing you the code changes in a pull/merge request. + +#### How the `merge base` is calculated + +To find the merge base build in Chromatic, we need to track back from the current latest build on a pull request until we find a build that was on the base branch. We then check that the build matches what's in the git commit history (keep in mind you may not have run a build for every single commit!). Learn more about how we use [ancestor builds](#find-the-ancestor-builds). Typically this leads to a situation like so: @@ -188,7 +208,8 @@ In this case the merge base starting at `q` will be `z`. It makes sense to use ` One thing to note is that if the merge base is quite old, the left hand side of the comparison may be quite old versions of your components. To work around this, merging (or rebasing) the base branch into the feature branch will resolve the issue, as demonstrated above. -### Patch builds +
+What happens when the merge base build isn't found? (patch builds) If Chromatic searches for a merge base and doesn't find one, it will prompt you to create a "patch build". This situation typically comes about when you are first installing Chromatic and you don't have a build for older, historical commits (like the commit `x` in the picture above). @@ -200,3 +221,5 @@ The Chromatic CLI has a special option `--patch-build=$head...$base` which is in 4. Put your repository back as it was before. Essentially we are retroactively creating the merge base build, so we have something to compare against. + +
diff --git a/src/content/snapshot/turbosnap.mdx b/src/content/snapshot/turbosnap.mdx index 792164ad..4106b68a 100644 --- a/src/content/snapshot/turbosnap.mdx +++ b/src/content/snapshot/turbosnap.mdx @@ -40,7 +40,7 @@ It will build and test stories that may have been affected by the Git changes si ### How it works -1. Chromatic considers the Git changes between the current commit and the commit of the [ancestor build](/docs/branching-and-baselines#calculating-the-ancestor-builds). +1. Chromatic considers the Git changes between the current commit and the commit of the [ancestor build](/docs/branching-and-baselines#find-the-ancestor-builds). 2. Chromatic then uses Webpack's dependency graph to track those changes back up to the story files that depend on them. 3. Chromatic only tests the stories defined in those story files, as well as any tests that were denied on the parent build. @@ -242,7 +242,11 @@ Once TurboSnap is activated, all subsequent builds will display an indicator wit TurboSnap will make working in a monorepo more efficient. Because it detects affected stories based on the actual files changed, pushing a commit that touched only backend code will run faster in CI and not use up your snapshot quota. However, it will still build and publish your Storybook. To avoid that, you can [skip Chromatic entirely](/docs/monorepos#only-run-chromatic-when-changes-occur-in-a-subproject), speeding up your CI pipeline even more. - + If your monorepo has stories from multiple subprojects coming together in one Storybook, you might currently [run Chromatic on a subset of your Storybook](/docs/monorepos#run-tests-on-a-subset-of-stories). With TurboSnap enabled, that happens automatically. You'll be able to build and publish your entire Storybook, but Chromatic won't test unchanged subprojects or take snapshots of those stories. You no longer need to build a subset of your Storybook manually. @@ -363,13 +367,13 @@ If you have a large dependency tree, the build process may fail due to an out of
Why do merge commits test more changes than I expect? -Ordinarily, TurboSnap uses git to find all files that have changed since the [ancestor build](/docs/branching-and-baselines#calculating-the-ancestor-builds) to determine which components/stories to snapshot. The changed file behavior is more complex with merge commits because there are two "ancestor builds". +Ordinarily, TurboSnap uses git to find all files that have changed since the [ancestor build](/docs/branching-and-baselines#find-the-ancestor-builds) to determine which components/stories to snapshot. The changed file behavior is more complex with merge commits because there are two "ancestor builds". When you have a merge commit, Chromatic considers **any file that has changed since either ancestor's commit** to decide if a story needs to be re-snapshotted. In other words, the union of the git changes. The reason for this behavior relates to what Chromatic does when it chooses not to re-snapshot a story. In such case, it "copies" the snapshot for the story from the ancestor build, knowing (due to the git check) that the story cannot have changed in the meantime. -In the case of merge commits, Chromatic does not know ahead of time which side of the merge the snapshot might be copied from because that involves running the [complete baseline selection](/docs/branching-and-baselines#calculating-a-snapshot-baseline-from-the-ancestor-builds) process, so it needs to be conservative and allow for changes on either branch. +In the case of merge commits, Chromatic does not know ahead of time which side of the merge the snapshot might be copied from because that involves running the [complete baseline selection](/docs/branching-and-baselines#calculate-a-snapshot-baseline-from-the-ancestor-builds) process, so it needs to be conservative and allow for changes on either branch.
@@ -380,7 +384,14 @@ TurboSnap is compatible with squash and merge rebasing as of version 6.6+. Pleas - - - - \ No newline at end of file + + + From 867373e206335053625c98bc5eb9792e535ce851 Mon Sep 17 00:00:00 2001 From: domyen Date: Wed, 29 Nov 2023 15:28:41 -0500 Subject: [PATCH 2/7] Typo --- src/content/snapshot/branching-and-baselines.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/content/snapshot/branching-and-baselines.md b/src/content/snapshot/branching-and-baselines.md index df1a92e9..f31ce1e4 100644 --- a/src/content/snapshot/branching-and-baselines.md +++ b/src/content/snapshot/branching-and-baselines.md @@ -180,7 +180,7 @@ UI Review generates changesets using the most recent snapshots on a build compar Conceptually, this is similar to what systems like GitHub do when showing you the code changes in a pull/merge request. -#### How the `merge base` is calculated +#### How is `merge base` calculated To find the merge base build in Chromatic, we need to track back from the current latest build on a pull request until we find a build that was on the base branch. We then check that the build matches what's in the git commit history (keep in mind you may not have run a build for every single commit!). Learn more about how we use [ancestor builds](#find-the-ancestor-builds). From 0acb71c6d9787bd00cd4e14f58047f47e8350231 Mon Sep 17 00:00:00 2001 From: Dan Manchester Date: Wed, 29 Nov 2023 15:20:12 -0800 Subject: [PATCH 3/7] Fixes typo and punctuation nits in branching-and-baselines.md --- .../snapshot/branching-and-baselines.md | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/content/snapshot/branching-and-baselines.md b/src/content/snapshot/branching-and-baselines.md index f31ce1e4..ff06a99b 100644 --- a/src/content/snapshot/branching-and-baselines.md +++ b/src/content/snapshot/branching-and-baselines.md @@ -16,11 +16,11 @@ This document breaks down how Chromatic decides what snapshots to compare when u UI Tests serve as visual regression tests between builds, similar to snapshot testing. When modifications are made in a PR, the tests fail until the new snapshots are accepted as baselines. -### What's a baseline? +### What’s a baseline? A baseline is the last known “good” state of the story in a given [mode](/docs/modes). [UI Tests](/docs/test) take snapshots and compare them to baselines to detect changes. -Chromatic's objective is to maintain an up to date "baseline" for each story. Baselines live alongside the git history and persist through git branching and merging. +Chromatic’s objective is to maintain an up to date “baseline” for each story. Baselines live alongside the git history and persist through git branching and merging. Baselines only update when changes are [accepted](/docs/test#verify-ui-changes) by you or your team. Since baselines are tracked independently for each branch, when you merge that branch into another (for instance back into `main`), the baseline comes with it. @@ -45,7 +45,7 @@ When you merge two branches together, Chromatic can sometimes have two (or more)
What happens to baselines when a branch is rebased? -If you rebase a branch (say updating to branch off the latest commit off `main`), then you create a new commit that isn't a git descendent of the previous (pre-rebase) commit on that branch. Conceptually, that might mean that Chromatic should throw away any approvals that were made for builds on the branch, however this is probably not what you want. +If you rebase a branch (say updating to branch off the latest commit off `main`), then you create a new commit that isn’t a git descendent of the previous (pre-rebase) commit on that branch. Conceptually, that might mean that Chromatic should throw away any approvals that were made for builds on the branch, however this is probably not what you want. For this reason, we _always_ include accepted baselines from the latest build on the current branch, regardless of git history. You can bypass this with the `--ignore-last-build-on-branch=` flag of `chromatic`. For example: @@ -66,11 +66,11 @@ Read our CI [documentation](/docs/ci). Chromatic detects squash and rebase merges. Your baselines are preserved between branches, even when squashing or rebasing wipes the Git history. -If you use the "squash" or "rebase" merge feature on Pull Requests, then a commit is created on your base branch that is not a descendant of the commits for the PR. See the diagram below. +If you use the “squash” or “rebase” merge feature on Pull Requests, then a commit is created on your base branch that is not a descendant of the commits for the PR. See the diagram below. ![Squash and rebase merges remove Git history](../../images/squash-merge.png) -This means Chromatic has no way to tell, using Git, that baselines accepted during the PR should "come over" to the main branch. Instead, we use Git provider APIs to detect this situation. When running the squash/rebase merge commit we'll use the accepted baselines of the _most recent_ commit on the head branch of the PR. +This means Chromatic has no way to tell, using Git, that baselines accepted during the PR should “come over” to the main branch. Instead, we use Git provider APIs to detect this situation. When running the squash/rebase merge commit, we’ll use the accepted baselines of the _most recent_ commit on the head branch of the PR. If you are using GitHub, you need to enable our GitHub App (on the [Pull Request](/docs/review) screen) for this feature to work. Bitbucket and GitLab will work out of the box. @@ -78,7 +78,7 @@ If you are using GitHub, you need to enable our GitHub App (on the [Pull Request ### How are baselines calculated? -In Chromatic, a build contains of a set of snapshots, each of which is a snapshot of a single story in a single mode. The baseline is the last accepted snapshot on a given branch. Each branch has builds associated with it, so to find the baseline, we need to traverse git history for that branch to find the "ancestor" build. +In Chromatic, a build contains of a set of snapshots, each of which is a snapshot of a single story in a single mode. The baseline is the last accepted snapshot on a given branch. Each branch has builds associated with it, so to find the baseline, we need to traverse git history for that branch to find the “ancestor” build. #### Find the ancestor build(s) @@ -146,7 +146,7 @@ Then in Build N+2, we should compare the “new” green buttons to the original
How do browsers work with baselines? -Baselines are calculated above the level of browsers. Clicking "accept" on a baseline will accept all snapshots taken in different browsers associated with that baseline. We can ignore multiple browsers when thinking about baselines. +Baselines are calculated above the level of browsers. Clicking “accept” on a baseline will accept all snapshots taken in different browsers associated with that baseline. We can ignore multiple browsers when thinking about baselines.
@@ -160,13 +160,13 @@ In the case that there are multiple ancestor builds, the algorithm to calculate
How do I visualize baseline history for a story? -When you [verify UI Test changes on Chromatic](/docs/test#verify-ui-changes), you'll see a historical set of baselines that correspond to the algorithm above. This helps you understand when the baseline changed, by who, and in which commit. +When you [verify UI Test changes on Chromatic](/docs/test#verify-ui-changes), you’ll see a historical set of baselines that correspond to the algorithm above. This helps you understand when the baseline changed, by who, and in which commit. -The snapshot marked "Most recent build...." is a change that hasn't been accepted as a baseline yet. The baseline marked "current baseline" is the last known good version of the snapshot that was accepted by Tom Coleman. Going back in the timeline, the listed baselines show previous times the component changed. +The snapshot marked “Most recent build....” is a change that hasn’t been accepted as a baseline yet. The baseline marked “current baseline” is the last known good version of the snapshot that was accepted by Tom Coleman. Going back in the timeline, the listed baselines show previous times the component changed.
@@ -182,7 +182,7 @@ Conceptually, this is similar to what systems like GitHub do when showing you th #### How is `merge base` calculated -To find the merge base build in Chromatic, we need to track back from the current latest build on a pull request until we find a build that was on the base branch. We then check that the build matches what's in the git commit history (keep in mind you may not have run a build for every single commit!). Learn more about how we use [ancestor builds](#find-the-ancestor-builds). +To find the merge base build in Chromatic, we need to track back from the current latest build on a pull request until we find a build that was on the base branch. We then check that the build matches what’s in the git commit history (keep in mind you may not have run a build for every single commit!). Learn more about how we use [ancestor builds](#find-the-ancestor-builds). Typically this leads to a situation like so: @@ -192,7 +192,7 @@ x - y - z [base] w - p - q [head] ``` -Starting with the build corresponding to commit `q`, Chromatic walks back the commit and build history, through `p` and `w` until it reaches `x`. This is the "merge base" build (and also commit, which would be output from `git merge-base base head`). +Starting with the build corresponding to commit `q`, Chromatic walks back the commit and build history, through `p` and `w` until it reaches `x`. This is the “merge base” build (and also commit, which would be output from `git merge-base base head`). Chromatic will now compare the stories from `q` to the corresponding stories in `x` to generate the UI changes for the PR. @@ -209,15 +209,15 @@ In this case the merge base starting at `q` will be `z`. It makes sense to use ` One thing to note is that if the merge base is quite old, the left hand side of the comparison may be quite old versions of your components. To work around this, merging (or rebasing) the base branch into the feature branch will resolve the issue, as demonstrated above.
-What happens when the merge base build isn't found? (patch builds) +What happens when the merge base build isn’t found? (patch builds) -If Chromatic searches for a merge base and doesn't find one, it will prompt you to create a "patch build". This situation typically comes about when you are first installing Chromatic and you don't have a build for older, historical commits (like the commit `x` in the picture above). +If Chromatic searches for a merge base and doesn’t find one, it will prompt you to create a “patch build.” This situation typically comes about when you are first installing Chromatic and you don’t have a build for older, historical commits (like the commit `x` in the picture above). The Chromatic CLI has a special option `--patch-build=$head...$base` which is intended for this purpose. What this does is: 1. Figure out what the merge base commit between head and base is in your git repo. 2. Check out that commit and update dependencies -3. Run a Chromatic build for that commit, flagging to the server that is is a special "patch" build (so it doesn't affect [UI Tests](/docs/test) baselines). +3. Run a Chromatic build for that commit, flagging to the server that is is a special “patch” build (so it doesn’t affect [UI Tests](/docs/test) baselines). 4. Put your repository back as it was before. Essentially we are retroactively creating the merge base build, so we have something to compare against. From 8ca87f17e0acf52625c5d1e729d9e295e7857baf Mon Sep 17 00:00:00 2001 From: domyen Date: Wed, 29 Nov 2023 18:41:01 -0500 Subject: [PATCH 4/7] Address feedback from Todd, Tom, and Suki from Slack --- .../snapshot/branching-and-baselines.md | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/src/content/snapshot/branching-and-baselines.md b/src/content/snapshot/branching-and-baselines.md index f31ce1e4..a8d71208 100644 --- a/src/content/snapshot/branching-and-baselines.md +++ b/src/content/snapshot/branching-and-baselines.md @@ -7,11 +7,23 @@ sidebar: { order: 3 } # Branches, baselines, and git history -This document breaks down how Chromatic decides what snapshots to compare when using UI Tests or UI Review. +This document describes how Chromatic decides what snapshots to compare when using UI Tests or UI Review. - [UI Tests](#ui-tests-verify-changes-on-one-branch): Verify changes on one branch (uses baselines) - [UI Review](#ui-review-compare-two-branches-for-changes): Compare two branches for changes (does not use baselines) +```shell +[M] - A - B - C [base branch] + \ + D - E - [F] [head branch] +``` + +
+ +Visualize the different comparisons methods using the git history diagram above. **UI Tests** compares commit F to E, the previous commit in the git history or its direct ancestors. **UI Review** always compares commit F to M, the merge base. + +
+ ## UI Tests: Verify changes on one branch UI Tests serve as visual regression tests between builds, similar to snapshot testing. When modifications are made in a PR, the tests fail until the new snapshots are accepted as baselines. @@ -122,7 +134,7 @@ You can see the ancestor builds listed on the build page: Once we’ve got the ancestor builds for a build, the algorithm to calculate the baseline for any given snapshot goes like this: -If there is one ancestor build, find if there is a snapshot for the same story & viewport combination. +If there is one ancestor build, find if there is a snapshot for the same story & mode combination. If there is, check the status of that snapshot: @@ -141,6 +153,7 @@ z - Build N+2 ``` Suppose then in commit y, we changed the color of our submit buttons to be orange rather than green. However, we realized this was a mistake and denied the change. Then in commit z we changed the colour back to green. + Then in Build N+2, we should compare the “new” green buttons to the original green buttons (from Build N). If they are back to the way they were before, the build should pass without you needing to intervene. If the green color is a different shade, Chromatic should show you a diff and you can decide if that’s what you wanted.
@@ -176,9 +189,18 @@ Similar to a code review, UI Review shows what changes will occur on the base br ### How changesets are generated -UI Review generates changesets using the most recent snapshots on a build compared to the `merge base`. It does not use baselines to generate changesets so the changeset will show snapshots that are unreviewed (havent been accepted). +A changeset is a list of UI changes between two branches. [UI Review](/docs/review) compares the latest snapshots on a feature branch to the latest snapshots on the base branch (referenced as `merge base`) to generate a changeset. + +Unlike UI Tests, it does not use baselines to generate changesets so the list of changes may also show snapshots that were unreviewed in UI Tests. Conceptually, this is similar to what systems like GitHub do when showing you the code changes in a pull/merge request. + +
+What if I skip running Chromatic on a base branch? -Conceptually, this is similar to what systems like GitHub do when showing you the code changes in a pull/merge request. +UI Review will not work if you skip running `chromatic` on a base branch like `main`. Chromatic needs to have two builds to generate a changeset. + +UI Review works differently than UI Tests. Some customers skip running `chromatic` on their base branches in UI Tests. This works for UI Tests because our baseline detection algorithm. UI Review needs `chromatic` to run in order to create a build that captures snapshots for comparison. + +
#### How is `merge base` calculated From 7187ddb9645939322076b4db7cf98d3db3158c2d Mon Sep 17 00:00:00 2001 From: domyen Date: Wed, 29 Nov 2023 18:49:11 -0500 Subject: [PATCH 5/7] Be consistent with lowercasing Git --- src/content/snapshot/branching-and-baselines.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/content/snapshot/branching-and-baselines.md b/src/content/snapshot/branching-and-baselines.md index 6de66324..a9b58c10 100644 --- a/src/content/snapshot/branching-and-baselines.md +++ b/src/content/snapshot/branching-and-baselines.md @@ -76,13 +76,13 @@ Read our CI [documentation](/docs/ci).
How do baselines get preserved during squash and rebase-merging? -Chromatic detects squash and rebase merges. Your baselines are preserved between branches, even when squashing or rebasing wipes the Git history. +Chromatic detects squash and rebase merges. Your baselines are preserved between branches, even when squashing or rebasing wipes the git history. If you use the “squash” or “rebase” merge feature on Pull Requests, then a commit is created on your base branch that is not a descendant of the commits for the PR. See the diagram below. -![Squash and rebase merges remove Git history](../../images/squash-merge.png) +![Squash and rebase merges remove git history](../../images/squash-merge.png) -This means Chromatic has no way to tell, using Git, that baselines accepted during the PR should “come over” to the main branch. Instead, we use Git provider APIs to detect this situation. When running the squash/rebase merge commit, we’ll use the accepted baselines of the _most recent_ commit on the head branch of the PR. +This means Chromatic has no way to tell, using git, that baselines accepted during the PR should “come over” to the main branch. Instead, we use git provider APIs to detect this situation. When running the squash/rebase merge commit, we’ll use the accepted baselines of the _most recent_ commit on the head branch of the PR. If you are using GitHub, you need to enable our GitHub App (on the [Pull Request](/docs/review) screen) for this feature to work. Bitbucket and GitLab will work out of the box. From b15d3181d8ce1623d5af1397a3cb04760fe024d3 Mon Sep 17 00:00:00 2001 From: domyen Date: Wed, 29 Nov 2023 18:51:17 -0500 Subject: [PATCH 6/7] Clarify rebasing --- src/content/snapshot/branching-and-baselines.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/content/snapshot/branching-and-baselines.md b/src/content/snapshot/branching-and-baselines.md index a9b58c10..717862ff 100644 --- a/src/content/snapshot/branching-and-baselines.md +++ b/src/content/snapshot/branching-and-baselines.md @@ -57,7 +57,9 @@ When you merge two branches together, Chromatic can sometimes have two (or more)
What happens to baselines when a branch is rebased? -If you rebase a branch (say updating to branch off the latest commit off `main`), then you create a new commit that isn’t a git descendent of the previous (pre-rebase) commit on that branch. Conceptually, that might mean that Chromatic should throw away any approvals that were made for builds on the branch, however this is probably not what you want. +If you rebase a branch, then you create a new commit that isn’t a git descendent of the previous (pre-rebase) commit on that branch. For example, you update to branch off the latest commit off `main`. + +Conceptually, that might mean that Chromatic should throw away any approvals that were made for builds on the branch, however this is probably not what you want. For this reason, we _always_ include accepted baselines from the latest build on the current branch, regardless of git history. You can bypass this with the `--ignore-last-build-on-branch=` flag of `chromatic`. For example: From f066e6c8b98612426a7d2b94806b55d147b9b39c Mon Sep 17 00:00:00 2001 From: domyen Date: Wed, 29 Nov 2023 19:07:26 -0500 Subject: [PATCH 7/7] Remove baseline image and refine copy --- src/content/snapshot/branching-and-baselines.md | 6 +++--- src/images/baselines.jpg | Bin 27854 -> 0 bytes 2 files changed, 3 insertions(+), 3 deletions(-) delete mode 100644 src/images/baselines.jpg diff --git a/src/content/snapshot/branching-and-baselines.md b/src/content/snapshot/branching-and-baselines.md index 717862ff..04d72f84 100644 --- a/src/content/snapshot/branching-and-baselines.md +++ b/src/content/snapshot/branching-and-baselines.md @@ -38,8 +38,6 @@ Baselines only update when changes are [accepted](/docs/test#verify-ui-changes) Conceptually, this is akin to storing a snapshot file in your repository with each accepted change, though Chromatic does not actually do this. -![Baselines](../../images/baselines.jpg) -
What happens to baselines when there are multiple branches? @@ -193,7 +191,9 @@ Similar to a code review, UI Review shows what changes will occur on the base br A changeset is a list of UI changes between two branches. [UI Review](/docs/review) compares the latest snapshots on a feature branch to the latest snapshots on the base branch (referenced as `merge base`) to generate a changeset. -Unlike UI Tests, it does not use baselines to generate changesets so the list of changes may also show snapshots that were unreviewed in UI Tests. Conceptually, this is similar to what systems like GitHub do when showing you the code changes in a pull/merge request. +Conceptually, this is similar to what systems like GitHub do when showing you the code changes in a pull/merge request. + +Unlike UI Tests, UI Review does not use baselines to generate changesets so the list of changes may also show snapshots that were unreviewed in UI Tests.
What if I skip running Chromatic on a base branch? diff --git a/src/images/baselines.jpg b/src/images/baselines.jpg deleted file mode 100644 index 838ec7c1ee1ec91a4355f63215c8ad6f29755342..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 27854 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