Upload files to "/"
This commit is contained in:
@@ -23,7 +23,7 @@ If you are using separate jobs for generating common artifacts and sharing them
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: /install.sh
|
run: /install.sh
|
||||||
@@ -31,7 +31,7 @@ steps:
|
|||||||
- name: Build artifacts
|
- name: Build artifacts
|
||||||
run: /build.sh
|
run: /build.sh
|
||||||
|
|
||||||
- uses: actions/cache/save@v5
|
- uses: actions/cache/save@v3
|
||||||
id: cache
|
id: cache
|
||||||
with:
|
with:
|
||||||
path: path/to/dependencies
|
path: path/to/dependencies
|
||||||
@@ -47,7 +47,7 @@ Let's say we have a restore step that computes a key at runtime.
|
|||||||
#### Restore a cache
|
#### Restore a cache
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
uses: actions/cache/restore@v5
|
uses: actions/cache/restore@v3
|
||||||
id: restore-cache
|
id: restore-cache
|
||||||
with:
|
with:
|
||||||
key: cache-${{ hashFiles('**/lockfiles') }}
|
key: cache-${{ hashFiles('**/lockfiles') }}
|
||||||
@@ -55,7 +55,7 @@ with:
|
|||||||
|
|
||||||
#### Case 1 - Where a user would want to reuse the key as it is
|
#### Case 1 - Where a user would want to reuse the key as it is
|
||||||
```yaml
|
```yaml
|
||||||
uses: actions/cache/save@v5
|
uses: actions/cache/save@v3
|
||||||
with:
|
with:
|
||||||
key: ${{ steps.restore-cache.outputs.cache-primary-key }}
|
key: ${{ steps.restore-cache.outputs.cache-primary-key }}
|
||||||
```
|
```
|
||||||
@@ -63,54 +63,26 @@ with:
|
|||||||
#### Case 2 - Where the user would want to re-evaluate the key
|
#### Case 2 - Where the user would want to re-evaluate the key
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
uses: actions/cache/save@v5
|
uses: actions/cache/save@v3
|
||||||
with:
|
with:
|
||||||
key: npm-cache-${{hashfiles(package-lock.json)}}
|
key: npm-cache-${{hashfiles(package-lock.json)}}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Always save cache
|
### Always save cache
|
||||||
|
|
||||||
There are instances where some flaky test cases would fail the entire workflow and users would get frustrated because the builds would run for hours and the cache couldn't be saved as the workflow failed in between.
|
There are instances where some flaky test cases would fail the entire workflow and users would get frustrated because the builds would run for hours and the cache couldn't be saved as the workflow failed in between. For such use-cases, users now have the ability to use the `actions/cache/save` action to save the cache by using an `if: always()` condition. This way the cache will always be saved if generated, or a warning will be generated that nothing is found on the cache path. Users can also use the `if` condition to only execute the `actions/cache/save` action depending on the output of previous steps. This way they get more control of when to save the cache.
|
||||||
For such use-cases, users now have the ability to use the `actions/cache/save` action to save the cache by using an [`always()`](https://docs.github.com/actions/writing-workflows/choosing-what-your-workflow-does/expressions#always) condition.
|
|
||||||
This way the cache will always be saved if generated, or a warning will be generated that nothing is found on the cache path. Users can also use the `if` condition to only execute the `actions/cache/save` action depending on the output of previous steps. This way they get more control of when to save the cache.
|
|
||||||
|
|
||||||
To avoid saving a cache that already exists, the `cache-hit` output from a restore step should be checked.
|
|
||||||
|
|
||||||
The `cache-primary-key` output from the restore step should also be used to ensure
|
|
||||||
the cache key does not change during the build if it's calculated based on file contents.
|
|
||||||
|
|
||||||
Here's an example where we imagine we're calculating a lot of prime numbers and want to cache them:
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
name: Always Caching Prime Numbers
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
on: push
|
.
|
||||||
|
. // restore if need be
|
||||||
jobs:
|
.
|
||||||
build:
|
- name: Build
|
||||||
runs-on: ubuntu-latest
|
run: /build.sh
|
||||||
|
- uses: actions/cache/save@v3
|
||||||
steps:
|
if: always() // or any other condition to invoke the save action
|
||||||
- uses: actions/checkout@v6
|
|
||||||
|
|
||||||
- name: Restore cached Prime Numbers
|
|
||||||
id: cache-prime-numbers-restore
|
|
||||||
uses: actions/cache/restore@v5
|
|
||||||
with:
|
with:
|
||||||
key: ${{ runner.os }}-prime-numbers
|
path: path/to/dependencies
|
||||||
path: |
|
key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }}
|
||||||
path/to/dependencies
|
|
||||||
some/other/dependencies
|
|
||||||
|
|
||||||
# Intermediate workflow steps
|
|
||||||
|
|
||||||
- name: Always Save Prime Numbers
|
|
||||||
id: cache-prime-numbers-save
|
|
||||||
if: always() && steps.cache-prime-numbers-restore.outputs.cache-hit != 'true'
|
|
||||||
uses: actions/cache/save@v5
|
|
||||||
with:
|
|
||||||
key: ${{ steps.cache-prime-numbers-restore.outputs.cache-primary-key }}
|
|
||||||
path: |
|
|
||||||
path/to/dependencies
|
|
||||||
some/other/dependencies
|
|
||||||
```
|
```
|
||||||
|
|||||||
+24
-9
@@ -1,23 +1,38 @@
|
|||||||
name: 'Save a cache'
|
name: 'Restore Cache'
|
||||||
description: 'Save Cache artifacts like dependencies and build outputs to improve workflow execution time'
|
description: 'Restore Cache artifacts like dependencies and build outputs to improve workflow execution time'
|
||||||
author: 'GitHub'
|
author: 'GitHub'
|
||||||
inputs:
|
inputs:
|
||||||
path:
|
path:
|
||||||
description: 'A list of files, directories, and wildcard patterns to cache'
|
description: 'A list of files, directories, and wildcard patterns to restore'
|
||||||
required: true
|
required: true
|
||||||
key:
|
key:
|
||||||
description: 'An explicit key for saving the cache'
|
description: 'An explicit key for restoring the cache'
|
||||||
required: true
|
required: true
|
||||||
upload-chunk-size:
|
restore-keys:
|
||||||
description: 'The chunk size used to split up large files during upload, in bytes'
|
description: 'An ordered list of keys to use for restoring stale cache if no cache hit occurred for key. Note `cache-hit` returns false in this case.'
|
||||||
required: false
|
required: false
|
||||||
enableCrossOsArchive:
|
enableCrossOsArchive:
|
||||||
description: 'An optional boolean when enabled, allows windows runners to save caches that can be restored on other platforms'
|
description: 'An optional boolean when enabled, allows windows runners to restore caches that were saved on other platforms'
|
||||||
default: 'false'
|
default: 'false'
|
||||||
required: false
|
required: false
|
||||||
|
fail-on-cache-miss:
|
||||||
|
description: 'Fail the workflow if cache entry is not found'
|
||||||
|
default: 'false'
|
||||||
|
required: false
|
||||||
|
lookup-only:
|
||||||
|
description: 'Check if a cache entry exists for the given input(s) (key, restore-keys) without downloading the cache'
|
||||||
|
default: 'false'
|
||||||
|
required: false
|
||||||
|
outputs:
|
||||||
|
cache-hit:
|
||||||
|
description: 'A boolean value to indicate an exact match was found for the primary key'
|
||||||
|
cache-primary-key:
|
||||||
|
description: 'A resolved cache key for which cache match was attempted'
|
||||||
|
cache-matched-key:
|
||||||
|
description: 'Key of the cache that was restored, it could either be the primary key on cache-hit or a partial/complete match of one of the restore keys'
|
||||||
runs:
|
runs:
|
||||||
using: 'node24'
|
using: 'node16'
|
||||||
main: '../dist/save-only/index.js'
|
main: '../dist/restore-only/index.js'
|
||||||
branding:
|
branding:
|
||||||
icon: 'archive'
|
icon: 'archive'
|
||||||
color: 'gray-dark'
|
color: 'gray-dark'
|
||||||
|
|||||||
+1
-35
@@ -8,26 +8,17 @@ import * as testUtils from "../src/utils/testUtils";
|
|||||||
jest.mock("@actions/core");
|
jest.mock("@actions/core");
|
||||||
jest.mock("@actions/cache");
|
jest.mock("@actions/cache");
|
||||||
|
|
||||||
let pristineEnv: NodeJS.ProcessEnv;
|
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
pristineEnv = process.env;
|
|
||||||
jest.spyOn(core, "getInput").mockImplementation((name, options) => {
|
jest.spyOn(core, "getInput").mockImplementation((name, options) => {
|
||||||
return jest.requireActual("@actions/core").getInput(name, options);
|
return jest.requireActual("@actions/core").getInput(name, options);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
afterEach(() => {
|
||||||
jest.resetModules();
|
|
||||||
process.env = pristineEnv;
|
|
||||||
delete process.env[Events.Key];
|
delete process.env[Events.Key];
|
||||||
delete process.env[RefKey];
|
delete process.env[RefKey];
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
|
||||||
process.env = pristineEnv;
|
|
||||||
});
|
|
||||||
|
|
||||||
test("isGhes returns true if server url is not github.com", () => {
|
test("isGhes returns true if server url is not github.com", () => {
|
||||||
try {
|
try {
|
||||||
process.env["GITHUB_SERVER_URL"] = "http://example.com";
|
process.env["GITHUB_SERVER_URL"] = "http://example.com";
|
||||||
@@ -240,28 +231,3 @@ test("isCacheFeatureAvailable for ac disabled on dotcom", () => {
|
|||||||
delete process.env["GITHUB_SERVER_URL"];
|
delete process.env["GITHUB_SERVER_URL"];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("isGhes returns false when the GITHUB_SERVER_URL environment variable is not defined", async () => {
|
|
||||||
delete process.env["GITHUB_SERVER_URL"];
|
|
||||||
expect(actionUtils.isGhes()).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("isGhes returns false when the GITHUB_SERVER_URL environment variable is set to github.com", async () => {
|
|
||||||
process.env["GITHUB_SERVER_URL"] = "https://github.com";
|
|
||||||
expect(actionUtils.isGhes()).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("isGhes returns false when the GITHUB_SERVER_URL environment variable is set to a GitHub Enterprise Cloud-style URL", async () => {
|
|
||||||
process.env["GITHUB_SERVER_URL"] = "https://contoso.ghe.com";
|
|
||||||
expect(actionUtils.isGhes()).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("isGhes returns false when the GITHUB_SERVER_URL environment variable has a .localhost suffix", async () => {
|
|
||||||
process.env["GITHUB_SERVER_URL"] = "https://mock-github.localhost";
|
|
||||||
expect(actionUtils.isGhes()).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("isGhes returns true when the GITHUB_SERVER_URL environment variable is set to some other URL", async () => {
|
|
||||||
process.env["GITHUB_SERVER_URL"] = "https://src.onpremise.fabrikam.com";
|
|
||||||
expect(actionUtils.isGhes()).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -449,19 +449,3 @@ test("restore with lookup-only set", async () => {
|
|||||||
);
|
);
|
||||||
expect(failedMock).toHaveBeenCalledTimes(0);
|
expect(failedMock).toHaveBeenCalledTimes(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("restore failure with earlyExit should call process exit", async () => {
|
|
||||||
testUtils.setInput(Inputs.Path, "node_modules");
|
|
||||||
const failedMock = jest.spyOn(core, "setFailed");
|
|
||||||
const restoreCacheMock = jest.spyOn(cache, "restoreCache");
|
|
||||||
const processExitMock = jest.spyOn(process, "exit").mockImplementation();
|
|
||||||
|
|
||||||
// call restoreImpl with `earlyExit` set to true
|
|
||||||
await restoreImpl(new StateProvider(), true);
|
|
||||||
|
|
||||||
expect(restoreCacheMock).toHaveBeenCalledTimes(0);
|
|
||||||
expect(failedMock).toHaveBeenCalledWith(
|
|
||||||
"Input required and not supplied: key"
|
|
||||||
);
|
|
||||||
expect(processExitMock).toHaveBeenCalledWith(1);
|
|
||||||
});
|
|
||||||
|
|||||||
+2
-2
@@ -2,7 +2,7 @@ import * as cache from "@actions/cache";
|
|||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
|
|
||||||
import { Events, Inputs, RefKey } from "../src/constants";
|
import { Events, Inputs, RefKey } from "../src/constants";
|
||||||
import { saveRun } from "../src/saveImpl";
|
import run from "../src/save";
|
||||||
import * as actionUtils from "../src/utils/actionUtils";
|
import * as actionUtils from "../src/utils/actionUtils";
|
||||||
import * as testUtils from "../src/utils/testUtils";
|
import * as testUtils from "../src/utils/testUtils";
|
||||||
|
|
||||||
@@ -100,7 +100,7 @@ test("save with valid inputs uploads a cache", async () => {
|
|||||||
return Promise.resolve(cacheId);
|
return Promise.resolve(cacheId);
|
||||||
});
|
});
|
||||||
|
|
||||||
await saveRun();
|
await run();
|
||||||
|
|
||||||
expect(saveCacheMock).toHaveBeenCalledTimes(1);
|
expect(saveCacheMock).toHaveBeenCalledTimes(1);
|
||||||
expect(saveCacheMock).toHaveBeenCalledWith(
|
expect(saveCacheMock).toHaveBeenCalledWith(
|
||||||
|
|||||||
+12
-12
@@ -2,7 +2,7 @@ import * as cache from "@actions/cache";
|
|||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
|
|
||||||
import { Events, Inputs, RefKey } from "../src/constants";
|
import { Events, Inputs, RefKey } from "../src/constants";
|
||||||
import { saveImpl } from "../src/saveImpl";
|
import run from "../src/saveImpl";
|
||||||
import { StateProvider } from "../src/stateProvider";
|
import { StateProvider } from "../src/stateProvider";
|
||||||
import * as actionUtils from "../src/utils/actionUtils";
|
import * as actionUtils from "../src/utils/actionUtils";
|
||||||
import * as testUtils from "../src/utils/testUtils";
|
import * as testUtils from "../src/utils/testUtils";
|
||||||
@@ -77,7 +77,7 @@ test("save with invalid event outputs warning", async () => {
|
|||||||
const invalidEvent = "commit_comment";
|
const invalidEvent = "commit_comment";
|
||||||
process.env[Events.Key] = invalidEvent;
|
process.env[Events.Key] = invalidEvent;
|
||||||
delete process.env[RefKey];
|
delete process.env[RefKey];
|
||||||
await saveImpl(new StateProvider());
|
await run(new StateProvider());
|
||||||
expect(logWarningMock).toHaveBeenCalledWith(
|
expect(logWarningMock).toHaveBeenCalledWith(
|
||||||
`Event Validation Error: The event type ${invalidEvent} is not supported because it's not tied to a branch or tag ref.`
|
`Event Validation Error: The event type ${invalidEvent} is not supported because it's not tied to a branch or tag ref.`
|
||||||
);
|
);
|
||||||
@@ -100,7 +100,7 @@ test("save with no primary key in state outputs warning", async () => {
|
|||||||
});
|
});
|
||||||
const saveCacheMock = jest.spyOn(cache, "saveCache");
|
const saveCacheMock = jest.spyOn(cache, "saveCache");
|
||||||
|
|
||||||
await saveImpl(new StateProvider());
|
await run(new StateProvider());
|
||||||
|
|
||||||
expect(saveCacheMock).toHaveBeenCalledTimes(0);
|
expect(saveCacheMock).toHaveBeenCalledTimes(0);
|
||||||
expect(logWarningMock).toHaveBeenCalledWith(`Key is not specified.`);
|
expect(logWarningMock).toHaveBeenCalledWith(`Key is not specified.`);
|
||||||
@@ -115,7 +115,7 @@ test("save without AC available should no-op", async () => {
|
|||||||
|
|
||||||
const saveCacheMock = jest.spyOn(cache, "saveCache");
|
const saveCacheMock = jest.spyOn(cache, "saveCache");
|
||||||
|
|
||||||
await saveImpl(new StateProvider());
|
await run(new StateProvider());
|
||||||
|
|
||||||
expect(saveCacheMock).toHaveBeenCalledTimes(0);
|
expect(saveCacheMock).toHaveBeenCalledTimes(0);
|
||||||
});
|
});
|
||||||
@@ -128,7 +128,7 @@ test("save on ghes without AC available should no-op", async () => {
|
|||||||
|
|
||||||
const saveCacheMock = jest.spyOn(cache, "saveCache");
|
const saveCacheMock = jest.spyOn(cache, "saveCache");
|
||||||
|
|
||||||
await saveImpl(new StateProvider());
|
await run(new StateProvider());
|
||||||
|
|
||||||
expect(saveCacheMock).toHaveBeenCalledTimes(0);
|
expect(saveCacheMock).toHaveBeenCalledTimes(0);
|
||||||
});
|
});
|
||||||
@@ -161,7 +161,7 @@ test("save on GHES with AC available", async () => {
|
|||||||
return Promise.resolve(cacheId);
|
return Promise.resolve(cacheId);
|
||||||
});
|
});
|
||||||
|
|
||||||
await saveImpl(new StateProvider());
|
await run(new StateProvider());
|
||||||
|
|
||||||
expect(saveCacheMock).toHaveBeenCalledTimes(1);
|
expect(saveCacheMock).toHaveBeenCalledTimes(1);
|
||||||
expect(saveCacheMock).toHaveBeenCalledWith(
|
expect(saveCacheMock).toHaveBeenCalledWith(
|
||||||
@@ -194,7 +194,7 @@ test("save with exact match returns early", async () => {
|
|||||||
});
|
});
|
||||||
const saveCacheMock = jest.spyOn(cache, "saveCache");
|
const saveCacheMock = jest.spyOn(cache, "saveCache");
|
||||||
|
|
||||||
await saveImpl(new StateProvider());
|
await run(new StateProvider());
|
||||||
|
|
||||||
expect(saveCacheMock).toHaveBeenCalledTimes(0);
|
expect(saveCacheMock).toHaveBeenCalledTimes(0);
|
||||||
expect(infoMock).toHaveBeenCalledWith(
|
expect(infoMock).toHaveBeenCalledWith(
|
||||||
@@ -221,7 +221,7 @@ test("save with missing input outputs warning", async () => {
|
|||||||
});
|
});
|
||||||
const saveCacheMock = jest.spyOn(cache, "saveCache");
|
const saveCacheMock = jest.spyOn(cache, "saveCache");
|
||||||
|
|
||||||
await saveImpl(new StateProvider());
|
await run(new StateProvider());
|
||||||
|
|
||||||
expect(saveCacheMock).toHaveBeenCalledTimes(0);
|
expect(saveCacheMock).toHaveBeenCalledTimes(0);
|
||||||
expect(logWarningMock).toHaveBeenCalledWith(
|
expect(logWarningMock).toHaveBeenCalledWith(
|
||||||
@@ -259,7 +259,7 @@ test("save with large cache outputs warning", async () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
await saveImpl(new StateProvider());
|
await run(new StateProvider());
|
||||||
|
|
||||||
expect(saveCacheMock).toHaveBeenCalledTimes(1);
|
expect(saveCacheMock).toHaveBeenCalledTimes(1);
|
||||||
expect(saveCacheMock).toHaveBeenCalledWith(
|
expect(saveCacheMock).toHaveBeenCalledWith(
|
||||||
@@ -306,7 +306,7 @@ test("save with reserve cache failure outputs warning", async () => {
|
|||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
|
|
||||||
await saveImpl(new StateProvider());
|
await run(new StateProvider());
|
||||||
|
|
||||||
expect(saveCacheMock).toHaveBeenCalledTimes(1);
|
expect(saveCacheMock).toHaveBeenCalledTimes(1);
|
||||||
expect(saveCacheMock).toHaveBeenCalledWith(
|
expect(saveCacheMock).toHaveBeenCalledWith(
|
||||||
@@ -349,7 +349,7 @@ test("save with server error outputs warning", async () => {
|
|||||||
throw new Error("HTTP Error Occurred");
|
throw new Error("HTTP Error Occurred");
|
||||||
});
|
});
|
||||||
|
|
||||||
await saveImpl(new StateProvider());
|
await run(new StateProvider());
|
||||||
|
|
||||||
expect(saveCacheMock).toHaveBeenCalledTimes(1);
|
expect(saveCacheMock).toHaveBeenCalledTimes(1);
|
||||||
expect(saveCacheMock).toHaveBeenCalledWith(
|
expect(saveCacheMock).toHaveBeenCalledWith(
|
||||||
@@ -392,7 +392,7 @@ test("save with valid inputs uploads a cache", async () => {
|
|||||||
return Promise.resolve(cacheId);
|
return Promise.resolve(cacheId);
|
||||||
});
|
});
|
||||||
|
|
||||||
await saveImpl(new StateProvider());
|
await run(new StateProvider());
|
||||||
|
|
||||||
expect(saveCacheMock).toHaveBeenCalledTimes(1);
|
expect(saveCacheMock).toHaveBeenCalledTimes(1);
|
||||||
expect(saveCacheMock).toHaveBeenCalledWith(
|
expect(saveCacheMock).toHaveBeenCalledWith(
|
||||||
|
|||||||
+3
-3
@@ -2,7 +2,7 @@ import * as cache from "@actions/cache";
|
|||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
|
|
||||||
import { Events, Inputs, RefKey } from "../src/constants";
|
import { Events, Inputs, RefKey } from "../src/constants";
|
||||||
import { saveOnlyRun } from "../src/saveImpl";
|
import run from "../src/saveOnly";
|
||||||
import * as actionUtils from "../src/utils/actionUtils";
|
import * as actionUtils from "../src/utils/actionUtils";
|
||||||
import * as testUtils from "../src/utils/testUtils";
|
import * as testUtils from "../src/utils/testUtils";
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@ test("save with valid inputs uploads a cache", async () => {
|
|||||||
return Promise.resolve(cacheId);
|
return Promise.resolve(cacheId);
|
||||||
});
|
});
|
||||||
|
|
||||||
await saveOnlyRun();
|
await run();
|
||||||
|
|
||||||
expect(saveCacheMock).toHaveBeenCalledTimes(1);
|
expect(saveCacheMock).toHaveBeenCalledTimes(1);
|
||||||
expect(saveCacheMock).toHaveBeenCalledWith(
|
expect(saveCacheMock).toHaveBeenCalledWith(
|
||||||
@@ -122,7 +122,7 @@ test("save failing logs the warning message", async () => {
|
|||||||
return Promise.resolve(cacheId);
|
return Promise.resolve(cacheId);
|
||||||
});
|
});
|
||||||
|
|
||||||
await saveOnlyRun();
|
await run();
|
||||||
|
|
||||||
expect(saveCacheMock).toHaveBeenCalledTimes(1);
|
expect(saveCacheMock).toHaveBeenCalledTimes(1);
|
||||||
expect(saveCacheMock).toHaveBeenCalledWith(
|
expect(saveCacheMock).toHaveBeenCalledWith(
|
||||||
|
|||||||
Reference in New Issue
Block a user