Skip to content

Commit

Permalink
Snapshot flow 2 (#1150)
Browse files Browse the repository at this point in the history
* Flow type jest-snapshot

Adds requisite type declarations and annotations to the jest-snapshot
modules. I’m not *at all* familiar with what this code is responsible
for, so I had to guess at a few types.

Open questions:

  - Is the new Jasmine type appropriate?
  - For SnapshotFile._content, can the value type be more specific?
  - Flow complains about Object.assign() if the 1st argument is a
literal that doesn’t contain all the keys trying to be assigned (see
getMatchers)

* Update some types.
  • Loading branch information
cpojer authored Jun 14, 2016
1 parent 6ef4a79 commit 90e942f
Show file tree
Hide file tree
Showing 6 changed files with 107 additions and 25 deletions.
50 changes: 36 additions & 14 deletions packages/jest-snapshot/src/SnapshotFile.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
*/
'use strict';

Expand All @@ -13,15 +15,30 @@ const path = require('path');
const prettyFormat = require('pretty-format');
const SNAPSHOT_EXTENSION = 'snap';

const ensureDirectoryExists = filePath => {
import type {Path} from 'types/Config';

export type SnapshotFileT = SnapshotFile;

export type MatchResult = {
actual: string,
expected: string,
pass: boolean,
};

type SaveStatus = {
deleted: boolean,
saved: boolean,
};

const ensureDirectoryExists = (filePath: Path) => {
try {
createDirectory(path.join(path.dirname(filePath)));
} catch (e) {}
};

const escape = string => string.replace(/\`/g, '\\`');

const fileExists = filePath => {
const fileExists = (filePath: Path): boolean => {
try {
return fs.statSync(filePath).isFile();
} catch (e) {}
Expand All @@ -30,40 +47,45 @@ const fileExists = filePath => {

class SnapshotFile {

constructor(filename) {
_content: {[key: string]: any};
_dirty: boolean;
_filename: Path;
_uncheckedKeys: Set;

constructor(filename: Path): void {
this._filename = filename;
this._dirty = false;

this._content = Object.create(null);
if (this.fileExists(filename)) {
try {
Object.assign(this._content, require(filename));
Object.assign(this._content, require.call(null, filename));
} catch (e) {}
}
this._uncheckedKeys = new Set(Object.keys(this._content));
}

hasUncheckedKeys() {
hasUncheckedKeys(): boolean {
return this._uncheckedKeys.size > 0;
}

fileExists() {
fileExists(): boolean {
return fileExists(this._filename);
}

removeUncheckedKeys() {
removeUncheckedKeys(): void {
if (this._uncheckedKeys.size) {
this._dirty = true;
this._uncheckedKeys.forEach(key => delete this._content[key]);
this._uncheckedKeys.clear();
}
}

serialize(data) {
serialize(data: any): string {
return prettyFormat(data);
}

save(update) {
save(update: boolean): SaveStatus {
const status = {
deleted: false,
saved: false,
Expand Down Expand Up @@ -92,15 +114,15 @@ class SnapshotFile {
return status;
}

has(key) {
has(key: string): boolean {
return this._content[key] !== undefined;
}

get(key) {
get(key: string): any {
return this._content[key];
}

matches(key, value) {
matches(key: string, value: any): MatchResult {
this._uncheckedKeys.delete(key);
const actual = this.serialize(value);
const expected = this.get(key);
Expand All @@ -111,7 +133,7 @@ class SnapshotFile {
};
}

add(key, value) {
add(key: string, value: any): void {
this._dirty = true;
this._uncheckedKeys.delete(key);
this._content[key] = this.serialize(value);
Expand All @@ -121,7 +143,7 @@ class SnapshotFile {

module.exports = {
SNAPSHOT_EXTENSION,
forFile(testPath) {
forFile(testPath: Path): SnapshotFile {
const snapshotsPath = path.join(path.dirname(testPath), '__snapshots__');

const snapshotFilename = path.join(
Expand Down
23 changes: 23 additions & 0 deletions packages/jest-snapshot/src/SnapshotState.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
*/
'use strict';

import type {SnapshotFileT} from './SnapshotFile';

export type SnapshotState = {
currentSpecName: string,
getCounter: (() => number),
incrementCounter: (() => number),
snapshot: SnapshotFileT,
added: number,
updated: number,
matched: number,
unmatched: number,
};
29 changes: 25 additions & 4 deletions packages/jest-snapshot/src/getMatchers.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,37 @@
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
*/
'use strict';

const JasmineFormatter = require('jest-util').JasmineFormatter;

module.exports = (filePath, options, jasmine, snapshotState) => ({
toMatchSnapshot: (util, customEquality) => {
import type {FailedAssertion} from 'types/TestResult';
import type {Jasmine} from 'types/Jasmine';
import type {Path} from 'types/Config';
import type {SnapshotState} from './SnapshotState';

type CompareResult = {
pass: boolean,
message: ?string,
};

module.exports = (
filePath: Path,
options: Object,
jasmine: Jasmine,
snapshotState: SnapshotState,
) => ({
toMatchSnapshot: (util: any, customEquality: any) => {
return {
negativeCompare() {
throw new Error(
'Jest: `.not` can not be used with `.toMatchSnapshot()`.'
);
},
compare(actual, expected) {
compare(actual: any, expected: any): CompareResult {
if (expected !== undefined) {
throw new Error(
'Jest: toMatchSnapshot() does not accept parameters.'
Expand Down Expand Up @@ -64,8 +81,12 @@ module.exports = (filePath, options, jasmine, snapshotState) => ({
const matcherName = 'toMatchSnapshot';
const formatter = new JasmineFormatter(jasmine, {global: {}}, {});
formatter.addDiffableMatcher(matcherName);

message = formatter
.formatMatchFailure(Object.assign({matcherName}, matches))
.formatMatchFailure(Object.assign(
({matcherName}: FailedAssertion),
matches
))
.replace(
'toMatchSnapshot:',
'toMatchSnapshot #' + (callCount + 1) + ':'
Expand Down
9 changes: 6 additions & 3 deletions packages/jest-util/src/JasmineFormatter.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,10 @@ class JasmineFormatter {
formatMatchFailure(result: FailedAssertion) {
let message;
if (this._diffableMatchers[result.matcherName]) {
const isNot =
'isNot' in result ? result.isNot : /not to /.test(result.message);
const isNot = !!('isNot' in result
? result.isNot
: /not to /.test(result.message || '')
);
message = this.formatDiffable(
result.matcherName,
isNot,
Expand All @@ -78,8 +80,9 @@ class JasmineFormatter {
// error message & stack live on 'trace' field in jasmine 1.3
const error = result.trace ? result.trace : result;
if (!this._config.noStackTrace && error.stack) {
const errorMessage = error.message || '';
message = error.stack
.replace(message, error.message)
.replace(message, errorMessage)
.replace(/^.*Error:\s*/, '');
}
return message;
Expand Down
12 changes: 12 additions & 0 deletions types/Jasmine.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
*/
'use strict';

export type Jasmine = Object;
9 changes: 5 additions & 4 deletions types/TestResult.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@ type Error = {

export type FailedAssertion = {
matcherName: string,
message: string,
message?: string,
trace?: Error,
actual: any,
expected: any,
isNot: boolean,
actual?: any,
pass?: boolean,
expected?: any,
isNot?: boolean,
};

export type Status = 'passed' | 'failed' | 'skipped' | 'pending';
Expand Down

1 comment on commit 90e942f

@kentaromiura
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

    • Is the new Jasmine type appropriate?
      no idea.
    • For SnapshotFile._content, can the value type be more specific?
      yes, SnapshotFile._content should be {[key: string]: string} as the key is always set by serialize which always returns a string.
    • Flow complains about Object.assign() if the 1st argument is a
      literal that doesn’t contain all the keys trying to be assigned (see
      getMatchers)

You can add optional types if some types are not always there, if instead you want to initialize it to null you can mark them as nullable.

Please sign in to comment.