From 01838cc522cf97004f4d36f656922ed87cb71af4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jare=C5=A1?= Date: Sat, 29 Aug 2026 09:24:48 +0200 Subject: [PATCH] Do not copy the removed lines from a diff code block A diff block shows the old line in red and the new line in green. The + and - glyphs come from CSS ::before, and pseudo-element content is not in the DOM, so the copy button handed you the old line and the new line with nothing to tell them apart. The tutorial evolves the same file over several pages, so that is a broken file rather than a cosmetic problem. Copying the -Force block on the coverage page produced two $Name parameters and took the whole suite down with "Missing ')' in function parameter list". Swizzle the copy button to drop the lines marked diff-remove. Rendering is untouched, the red lines stay on the page and just do not reach the clipboard. The other half is a title="path" on a block that is not the whole file. The tutorial teaches that a titled block is a file to save, so a three line excerpt or a "# ... the four existing It blocks ..." placeholder under that title deletes the rest of the file. Print the complete file where the title stays, and drop the title where the block is genuinely an excerpt. Also move the "this is an illustration" note in front of the blocks it applies to, it was sitting after them. Fix #459 --- .../CodeBlock/Buttons/CopyButton/index.js | 55 +++++++++++++++++++ tutorial/2-testing-a-module/1-setup.mdx | 4 +- .../2-testing-a-module/3-public-functions.mdx | 4 +- .../4-private-functions.mdx | 2 +- .../2-choosing-what-runs.mdx | 14 +++-- tutorial/4-mocking/3-verifying-calls.mdx | 51 ++++++++++++----- tutorial/6-code-coverage/1-measuring.mdx | 2 +- .../6-code-coverage/2-closing-the-gaps.mdx | 42 +++++++++++++- 8 files changed, 150 insertions(+), 24 deletions(-) create mode 100644 src/theme/CodeBlock/Buttons/CopyButton/index.js diff --git a/src/theme/CodeBlock/Buttons/CopyButton/index.js b/src/theme/CodeBlock/Buttons/CopyButton/index.js new file mode 100644 index 0000000..3d11126 --- /dev/null +++ b/src/theme/CodeBlock/Buttons/CopyButton/index.js @@ -0,0 +1,55 @@ +import React from 'react'; +import CopyButton from '@theme-original/CodeBlock/Buttons/CopyButton'; +import { + CodeBlockContextProvider, + useCodeBlockContext, +} from '@docusaurus/theme-common/internal'; + +// Set by the diff-remove magic comment declared in docusaurus.config.js. +const DIFF_REMOVE_CLASS = 'code-block-diff-remove-line'; + +/** + * Drops the lines marked with `diff-remove` from the copied text. + * + * A diff block shows the old line in red and the new line in green. The `-` and `+` + * glyphs come from CSS `::before` in custom.css, and pseudo-element content is not part + * of the DOM, so without this the copy button hands you the old line and the new line + * with nothing to tell them apart. The tutorial evolves the same file over several pages, + * so that is a broken file rather than a cosmetic problem. + * + * Rendering is untouched. The removed lines stay on the page, they just do not travel + * to the clipboard. + */ +function withoutRemovedLines(metadata) { + const removed = Object.entries(metadata.lineClassNames) + .filter(([, classNames]) => classNames.includes(DIFF_REMOVE_CLASS)) + .map(([lineIndex]) => Number(lineIndex)); + + if (removed.length === 0) { + return metadata; + } + + const removedLines = new Set(removed); + const code = metadata.code + .split('\n') + .filter((_, lineIndex) => !removedLines.has(lineIndex)) + .join('\n'); + + return {...metadata, code}; +} + +export default function CopyButtonWrapper(props) { + const {metadata, wordWrap} = useCodeBlockContext(); + const copyMetadata = withoutRemovedLines(metadata); + + if (copyMetadata === metadata) { + return ; + } + + // Re-provide the context so only the copy button sees the trimmed code. + return ( + + + + ); +} diff --git a/tutorial/2-testing-a-module/1-setup.mdx b/tutorial/2-testing-a-module/1-setup.mdx index 10e4dc9..ea93f59 100644 --- a/tutorial/2-testing-a-module/1-setup.mdx +++ b/tutorial/2-testing-a-module/1-setup.mdx @@ -61,9 +61,9 @@ New-ModuleManifest -Path ./Planetarium/Planetarium.psd1 ` -PowerShellVersion '5.1' ``` -Open the generated file and find the `FunctionsToExport` line: +Open the generated file and find the `FunctionsToExport` line. This is one line out of the manifest `New-ModuleManifest` just wrote, not a file to save: -```powershell title="Planetarium/Planetarium.psd1" +```powershell FunctionsToExport = '*' ``` diff --git a/tutorial/2-testing-a-module/3-public-functions.mdx b/tutorial/2-testing-a-module/3-public-functions.mdx index fd84d6d..9858009 100644 --- a/tutorial/2-testing-a-module/3-public-functions.mdx +++ b/tutorial/2-testing-a-module/3-public-functions.mdx @@ -17,9 +17,9 @@ Importing the module instead fixes both. The test then calls the function throug ## Importing the module -Change the `BeforeAll` in your test file: +Change the `BeforeAll` in `Planetarium/Public/Get-Planet.Tests.ps1`, and leave the `Describe` below it alone for now: -```powershell title="Planetarium/Public/Get-Planet.Tests.ps1" +```powershell BeforeAll { # diff-remove . $PSCommandPath.Replace('.Tests.ps1', '.ps1') diff --git a/tutorial/2-testing-a-module/4-private-functions.mdx b/tutorial/2-testing-a-module/4-private-functions.mdx index 6398e45..e52cbd5 100644 --- a/tutorial/2-testing-a-module/4-private-functions.mdx +++ b/tutorial/2-testing-a-module/4-private-functions.mdx @@ -65,7 +65,7 @@ The first test is the odd one out, and it is deliberate. It asserts the function ## Passing values in -A script block handed to `InModuleScope` does not inherit your test's variables. This does not work: +A script block handed to `InModuleScope` does not inherit your test's variables. The next block is here to show the mistake, it is not a step, and on its own outside an `It` it fails with `No modules named 'Planetarium' are currently loaded` rather than the empty `$name` it is meant to demonstrate: ```powershell $name = 'Earth' diff --git a/tutorial/3-organising-tests/2-choosing-what-runs.mdx b/tutorial/3-organising-tests/2-choosing-what-runs.mdx index e1908ef..176c400 100644 --- a/tutorial/3-organising-tests/2-choosing-what-runs.mdx +++ b/tutorial/3-organising-tests/2-choosing-what-runs.mdx @@ -12,14 +12,20 @@ Pester gives you different tools for that, and the difference matters: **filters The most used filter in Pester is tags. `-Tag` goes on `Describe`, `Context` or `It`, and is inherited by everything inside. Your two private-function files are a natural group — they reach into the module with `InModuleScope`, and they are the tests you would drop first if you only wanted to check the public surface: -```powershell title="Planetarium/Private/ConvertTo-AstronomicalUnit.Tests.ps1" +Add the tag to the `Describe` line in each of the two files, leaving the rest of each file as it is. + +In `Planetarium/Private/ConvertTo-AstronomicalUnit.Tests.ps1`: + +```powershell # diff-remove Describe 'ConvertTo-AstronomicalUnit' { # diff-add Describe 'ConvertTo-AstronomicalUnit' -Tag 'Internal' { ``` -```powershell title="Planetarium/Private/Test-PlanetName.Tests.ps1" +In `Planetarium/Private/Test-PlanetName.Tests.ps1`: + +```powershell # diff-remove Describe 'Test-PlanetName' { # diff-add @@ -58,6 +64,8 @@ Typical tags used in projects are `Slow`, `Integration`, `Unit`, `WindowsOnly` e ## Skipping tests +Both blocks in this section are illustrations rather than tests to add, Planetarium has nothing that needs skipping yet. + `-Skip` marks a test as not to be run, while keeping it visible in the output: ```powershell @@ -82,8 +90,6 @@ It 'Uses the Windows registry' -Skip:(-not $IsWindows) { On Windows this runs; everywhere else it reports as skipped instead of failing. That is how a cross-platform suite handles the parts that genuinely cannot run everywhere — and it is how you would keep the CI matrix in the last module green if the module ever grew a platform-specific feature. -Both `-Skip` examples above are illustrations rather than tests to add — Planetarium has nothing that needs skipping yet. - ## BeforeDiscovery Here is the catch, and it is the one thing on this page that trips people up. diff --git a/tutorial/4-mocking/3-verifying-calls.mdx b/tutorial/4-mocking/3-verifying-calls.mdx index 9b0a0b5..80b1294 100644 --- a/tutorial/4-mocking/3-verifying-calls.mdx +++ b/tutorial/4-mocking/3-verifying-calls.mdx @@ -11,8 +11,23 @@ A mock lets you control what a command returns. `Should-Invoke` lets you assert Add a third test to the `Get-Planet with mocked data` block, below the two you already have: ```powershell title="Planetarium/Public/Get-Planet.Mocking.Tests.ps1" +BeforeAll { + Import-Module "$PSScriptRoot/../Planetarium.psd1" -Force +} + Describe 'Get-Planet with mocked data' { - # ... BeforeAll and the two existing It blocks ... + BeforeAll { + Mock -ModuleName Planetarium Get-PlanetData { + @( + [PSCustomObject] @{ Name = 'Aiur'; Order = 1; DistanceFromSunKm = 100000000 } + [PSCustomObject] @{ Name = 'Shakuras'; Order = 2; DistanceFromSunKm = 200000000 } + ) + } + } + + It 'Returns whatever the data source provides' { + (Get-Planet).Name | Should-BeCollection @('Aiur', 'Shakuras') + } It 'Filters the mocked data the same way' { (Get-Planet -Name 'A*').Name | Should-Be 'Aiur' @@ -43,8 +58,24 @@ The `-ModuleName` rule from the previous page applies here too: you are asking a `-ParameterFilter` narrows a mock to calls whose arguments match, allowing you to customize responses for different calls. Let's give it a try: ```powershell title="Planetarium/Private/Get-PlanetData.Tests.ps1" +BeforeAll { + Import-Module "$PSScriptRoot/../Planetarium.psd1" -Force +} + Describe 'Get-PlanetData' -Tag 'Internal' { - # ... the 'Converts the CSV strings into numbers' test ... + It 'Converts the CSV strings into numbers' { + Mock -ModuleName Planetarium Import-Csv { + @([PSCustomObject] @{ Name = 'Aiur'; Order = '3'; DistanceFromSunKm = '149597870.7' }) + } + + InModuleScope Planetarium { + $planet = Get-PlanetData + $planet.Name | Should-Be 'Aiur' + $planet.Order | Should-Be 3 + $planet.Order | Should-HaveType ([int]) + $planet.DistanceFromSunKm | Should-HaveType ([double]) + } + } # diff-add-start It 'Reads the CSV shipped with the module' { @@ -68,19 +99,13 @@ That doubles as an assertion. If the module ever reads a different file, the fil A mock with a `-ParameterFilter` only applies to calls that match it. If a call reaches a mocked command and *nothing* matches, Pester throws rather than guessing — it will not quietly run the real command behind your back. -Let's break it on purpose to see how this works. Change `planets` to `moons` in the test you just added: +Let's break it on purpose to see how this works. In `Planetarium/Private/Get-PlanetData.Tests.ps1`, change `planets` to `moons` on the `-ParameterFilter` of the test you just added, and leave the rest of the file alone: -```powershell title="Planetarium/Private/Get-PlanetData.Tests.ps1" - It 'Reads the CSV shipped with the module' { - Mock -ModuleName Planetarium Import-Csv { - @([PSCustomObject] @{ Name = 'Aiur'; Order = '1'; DistanceFromSunKm = '100' }) +```powershell # diff-remove } -ParameterFilter { $Path -like '*planets.csv' } # diff-add } -ParameterFilter { $Path -like '*moons.csv' } - - # ... rest of the test unchanged ... - } ``` ```powershell @@ -102,15 +127,15 @@ This behavior is new in Pester v6. Previous versions called the original command ### Giving a mock a fallback -Sometimes you genuinely want "handle this specific case, and everything else generically". Say so explicitly by adding a second mock with no `-ParameterFilter` — an unfiltered mock matches any call, so it becomes the fallback: +Sometimes you genuinely want "handle this specific case, and everything else generically". Say so explicitly by adding a second mock with no `-ParameterFilter` — an unfiltered mock matches any call, so it becomes the fallback. + +The next block is an illustration, not a step. Read it, do not add it to Planetarium: your `Import-Csv` mock should intercept exactly one file, so leaving it filtered and unmatched-is-an-error is the behaviour you want. ```powershell Mock Get-Thing { 'default' } # everything else Mock Get-Thing { 'one' } -ParameterFilter { $Id -eq 1 } # the specific case ``` -The example above is only used for illustration. Your `Import-Csv` mock should intercept exactly one file, so leaving it filtered and unmatched-is-an-error is the behaviour you want. - :::tip See [Mocking](../../docs/usage/mocking#pesterboundparameters) for an example of using the default mock to call the original command. ::: diff --git a/tutorial/6-code-coverage/1-measuring.mdx b/tutorial/6-code-coverage/1-measuring.mdx index a4b27e6..126a780 100644 --- a/tutorial/6-code-coverage/1-measuring.mdx +++ b/tutorial/6-code-coverage/1-measuring.mdx @@ -49,7 +49,7 @@ Pester v6 uses a profiler-based tracer by default, which is fast enough to leave The module is at 100%, and it would be a mistake to read that as "fully tested". -Coverage measures execution, not assertion. This test would give `ConvertTo-AstronomicalUnit` full coverage while checking nothing at all: +Coverage measures execution, not assertion. This test would give `ConvertTo-AstronomicalUnit` full coverage while checking nothing at all. It is an illustration, do not add it: ```powershell It 'Runs' { diff --git a/tutorial/6-code-coverage/2-closing-the-gaps.mdx b/tutorial/6-code-coverage/2-closing-the-gaps.mdx index e8be2b6..aa29e00 100644 --- a/tutorial/6-code-coverage/2-closing-the-gaps.mdx +++ b/tutorial/6-code-coverage/2-closing-the-gaps.mdx @@ -39,8 +39,41 @@ $result.CodeCoverage.CommandsMissed | Format-Table Function, Line, StartColumn, The uncovered branch has two behaviours worth pinning: it refuses by default, and `-Force` overrides it. Add both tests at the bottom of the `Describe` block, below the four you already have: ```powershell title="Planetarium/Public/Export-PlanetReport.Tests.ps1" +BeforeAll { + Import-Module "$PSScriptRoot/../Planetarium.psd1" -Force +} + Describe 'Export-PlanetReport' { - # ... the four existing It blocks ... + It 'Creates the report file' { + $path = Join-Path $TestDrive 'report.txt' + + Test-Path -Path $path | Should-BeFalse + Export-PlanetReport -Path $path + Test-Path -Path $path | Should-BeTrue + } + + It 'Writes one line per planet' { + $path = Join-Path $TestDrive 'all.txt' + + Export-PlanetReport -Path $path + + (Get-Content -Path $path).Count | Should-Be 8 + } + + It 'Writes the name and the distance in astronomical units' { + $path = Join-Path $TestDrive 'earth.txt' + + Export-PlanetReport -Path $path -Name 'Earth' + + Get-Content -Path $path | Should-Be 'Earth 1 AU' + } + + It 'Throws when no planet matches' { + $path = Join-Path $TestDrive 'nothing.txt' + + { Export-PlanetReport -Path $path -Name 'Pluto' } | + Should-Throw -ExceptionMessage "No planets matched 'Pluto'." + } # diff-add-start It 'Refuses to overwrite an existing report' { @@ -86,10 +119,17 @@ Back at 100% coverage. Enjoy this rare moment. When Code Coverage is enabled it writes a report to `./coverage.xml` by default that can be used by CI systems and other coverage reporting tools. You control the path using the `CodeCoverage.OutputPath` option - we'll just set the default explicit: ```powershell title="test.ps1" +$config = New-PesterConfiguration +$config.Run.Path = './Planetarium' +$config.Output.Verbosity = 'Detailed' +$config.TestResult.Enabled = $true +$config.TestResult.OutputPath = './testResults.xml' $config.CodeCoverage.Enabled = $true $config.CodeCoverage.Path = './Planetarium' # diff-add $config.CodeCoverage.OutputPath = './coverage.xml' + +Invoke-Pester -Configuration $config ``` ```xml title="coverage.xml"