Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove more internal angular attributes from snapshots #336

Closed
ravishivt opened this issue Dec 21, 2019 · 17 comments · Fixed by #343
Closed

Remove more internal angular attributes from snapshots #336

ravishivt opened this issue Dec 21, 2019 · 17 comments · Fixed by #343

Comments

@ravishivt
Copy link

Currently, snapshots often contain internal angular attribute values that cause tests to fail.
PR #97 fixed some of these but there are still more. The remaining ones were discussed in that PR but were shelved at that time. This new issue tracks the need to continue where that PR left off and remove the additional attribute + attribute values.

This issue is especially problematic when working with Angular Material. The snapshots will often contain one of the following attribute+values where the numeric indexes change.

  • class=ng-tns-c25-1
  • aria-owns=mat-input-4
  • aria-labelledby=mat-input-4
  • aria-controls=cdk-step-content-2-0
  • id=cdk-step-content-0-0
  • for=mat-form-field-label-9

Interestingly, the snapshots don't error on repeated executions of the same tests since the numeric indexes remain constant. However, if you rearrange the order of the same tests or add new tests before existing tests, the indexes change and snapshots fail. It appears Angular globally stores an index counter (called a namespaceId) and sequentially increments them for each render. If we had a way to reset this global state before each test, then this issue wouldn't be a problem since the indexes would be consistent. I was not able to find a way to do this albeit multiple attempts.

A quick workaround is to add a local Serializer and reference it in snapshotSerializers. Source code below:

AngularStripAnimationAttributesSnapshotSerializer.ts
'use strict';

const jestDOMElementSerializer = require('pretty-format').plugins.DOMElement;

const attributesToClean: { [key: string]: RegExp[] } = {
  class: [/^.*-\w*\d+-\d+$/], // e.g. "ng-tns-c25-1"
  id: [/^.*-\d+$/], // e.g. "mat-input-4", "cdk-step-content-0-0"
  for: [/^.*-\d+$/], // e.g. "mat-form-field-label-9"
  'aria-owns': [/^.*-\d+$/], // e.g. "mat-input-4"
  'aria-labelledby': [/^.*-\d+$/], // e.g. "mat-input-4", "cdk-step-label-0-0"
  'aria-controls': [/^.*-\d+$/], // e.g. "cdk-step-content-2-0"
};

const hasAttributesToClean = (attribute: Attr) => {
  return Object.keys(attributesToClean).some(
    removePatternKey => attribute.name === removePatternKey,
  );
};

const serialize = (node: Element, ...rest: any[]) => {
  const nodeCopy = node.cloneNode(true) as Element;
  Object.values(nodeCopy.attributes)
    .filter(hasAttributesToClean)
    .forEach((attribute: Attr) => {
      attribute.value = attribute.value
        .split(' ')
        .filter((attrValue: string) => {
          return !attributesToClean[attribute.name].some(attributeCleanRegex =>
            attributeCleanRegex.test(attrValue),
          );
        })
        .join(' ');
      nodeCopy.attributes.setNamedItem(attribute);
    });

  return jestDOMElementSerializer.serialize(nodeCopy, ...rest);
};

const serializeTestFn = (val: Element) => {
  return (
    val.attributes !== undefined &&
    Object.values(val.attributes).some(hasAttributesToClean)
  );
};
const test1 = (val: Element) =>
  jestDOMElementSerializer.test(val) && serializeTestFn(val);

module.exports = {
  serialize: serialize,
  test: test1,
};
@wtho
Copy link
Collaborator

wtho commented Jan 20, 2020

I would like to ask you to add these to the AngularNoNgAttributesSnapshotSerializer as I believe this will help others as well, a PR is very welcome!

@wtho
Copy link
Collaborator

wtho commented Mar 31, 2020

Just to keep track:
Since ivy (Angular v9), this internal attribute is also serialized:

  __ngContext__={[Function LRootView]}

@JohnYoungers
Copy link

Woof, I wish I would have ran into this an hour ago. I ended up creating this, but I'm going to pull in what you have since it's more comprehensive

const classRegexes = [/^ng-tns-.+$/, /ng-star-inserted/];

module.exports = {
  print: (node: HTMLElement, serialize: (node: Node) => string) => {
    const clone = node.cloneNode(true) as HTMLElement;

    const matchingClasses = Array.from(clone.classList).filter((c) => classRegexes.some((r) => r.test(c)));
    clone.classList.remove(...matchingClasses);

    return serialize(clone);
  },
  test: (node: HTMLElement) => {
    const classes = Array.from(node.classList ?? []);

    return classes.some((c) => classRegexes.some((r) => r.test(c)));
  },
};

@wtho
Copy link
Collaborator

wtho commented Apr 10, 2020

PRs to enhance the currently included serializer are welcome!

@JohnYoungers
Copy link

@wtho - after I posted my last note I realized it wasn't working quite the way I wanted it to: there were still nodes that contained the values I wanted to strip.

It looks like the issue is coming from the other serializers: by using 'serialize' opposed to 'print' it seems like some nodes don't end up making it further down the pipeline.

I reordered my serializers so that this one comes first:

  snapshotSerializers: [
    './jest/ng-id-stripper.serializer.ts',
    'jest-preset-angular/build/AngularNoNgAttributesSnapshotSerializer.js',
    'jest-preset-angular/build/AngularSnapshotSerializer.js',
    'jest-preset-angular/build/HTMLCommentSerializer.js',
  ],

And updated my prior version to use print opposed to serialize:

const attributesToClean: { [key: string]: RegExp[] } = {
  class: [/^ng-tns-.+$/, /^ng-star-inserted$/], // e.g. "ng-tns-c25-1", "ng-star-inserted"
  id: [/^.*-\d+$/, /^root\d+$/], // e.g. "mat-input-4", "cdk-step-content-0-0", "root0"
  for: [/^.*-\d+$/], // e.g. "mat-form-field-label-9"
  'aria-owns': [/^.*-\d+$/], // e.g. "mat-input-4"
  'aria-labelledby': [/^.*-\d+$/], // e.g. "mat-input-4", "cdk-step-label-0-0"
  'aria-controls': [/^.*-\d+$/], // e.g. "cdk-step-content-2-0"
};
const attributesToCleanKeys = Object.keys(attributesToClean);
const hasAttributesToClean = (attribute: Attr) => attributesToCleanKeys.some((name) => attribute.name === name);

let lastCleanedNode: Element | null = null;

module.exports = {
  print: (val: Element, serialize: (v: Element) => string) => {
    const clone = val.cloneNode(true) as Element;

    for (const attr of Object.values(clone.attributes).filter(hasAttributesToClean)) {
      attr.value = attr.value
        .split(' ')
        .filter((attrValue: string) => {
          return !attributesToClean[attr.name].some((regex) => regex.test(attrValue));
        })
        .join(' ');
    }

    lastCleanedNode = clone;
    return serialize(clone);
  },
  test: (val: Element) => val !== lastCleanedNode && val.attributes !== undefined && Object.values(val.attributes).some(hasAttributesToClean),
};

Going this route I was also able to add in that root0 removal.

At this point I think it's doing what I need it to do, but still a WIP: Opposed to just testing the node to see if it's the one we just cleaned, a better solution may be to have the test actually check to see if there's something to remove, but that would be a less efficient solution I think

@wtho wtho closed this as completed in #343 May 7, 2020
@yherasymchukarchitech
Copy link

yherasymchukarchitech commented Apr 20, 2021

I'm wondering, is it an issue that gone with 8.4.0 and nobody has it, or it's still there and more PRs should be created for it?

Indeed with Ivy snapshots have unexpected output, watch mode/cache param could show and hide __ngContext__

@ahnpnl
Copy link
Collaborator

ahnpnl commented Apr 20, 2021

The current serializer is still open for improvement :) feel free to submit a PR

@yherasymchukarchitech
Copy link

yherasymchukarchitech commented Apr 20, 2021

Interesting that without --watch mode, __ngContext__ does not appear in snapshots, I will do a further investigation about it

Update:
I can confirm that updating from jest@26.4.2 to jest@26.6.3
with jest-preset-angular@8.3.2 doesn't show the __ngContext__ but this is far from the truth, I think it's more an issue with caching in jest, than versions

@pillsilly
Copy link

Interesting that without --watch mode, __ngContext__ does not appear in snapshots, I will do a further investigation about it

Update:
I can confirm that updating from jest@26.4.2 to jest@26.6.3
with jest-preset-angular@8.3.2 doesn't show the __ngContext__ but this is far from the truth, I think it's more an issue with caching in jest, than versions

in my case the diff is not only about ngContext

there are blank spaces are added as well.

but the trigger is not about --watch but diff env: ci/local

haven't had time to go deep😉

@wtho
Copy link
Collaborator

wtho commented May 9, 2021

@yherasymchukarchitech @pillsilly Would be great if you manage to create a simple reproduction and open a new issue so we can investigate and fix this problem with you.

@JohnYoungers
Copy link

@pillsilly assuming you mean you're seeing inconsistency in the screenshots, one issue we ran into earlier is you need to make sure the environments are consistent from an ivy compilation standpoint.

Locally everything's probably compiled, but that might not be the case in the CI: you may want to make sure you run ngcc prior to running the tests

@pillsilly
Copy link

thank you guys,
@jayoungers
I found it true that in our CI environment before test starts there's no ngcc compliing log printed, though I kinda surprise because won't ngcc be called after installation (npm ci)?

but now my problem if that I want to reproduce the snapshot (I mean generate snapshot without ngcc being done)
how can I achieve that without clone the repo with --ignore-scripts (to avoid ngcc)
is there any way to clean up the ngcc results ?

@Sebastian-G
Copy link

Just to keep track:
Since ivy (Angular v9), this internal attribute is also serialized:

Since Angular 14
__ngContext__={[Function LRootView]} -> __ngContext__={[Function Number]}

@jahusa02
Copy link

I still have problems. When i run all my test at once, the snapshot has __ngContext__={[Function Number]}. If i run only the single test with the snapshot, the snapshot becomes __ngContext__="0"

@Sebastian-G
Copy link

@boch3n
Copy link

boch3n commented Sep 28, 2022

Ok, the problem is still not solved... Does anyone have an idea how to fix this?

@matthewdeanmartin
Copy link

matthewdeanmartin commented May 5, 2023

const attributesToClean: { [key: string]: RegExp[] } = {
  class: [/^ng-tns-.+$/, /^ng-star-inserted$/], // e.g. "ng-tns-c25-1", "ng-star-inserted"
  id: [/^.*-\d+$/, /^root\d+$/], // e.g. "mat-input-4", "cdk-step-content-0-0", "root0"
  for: [/^.*-\d+$/], // e.g. "mat-form-field-label-9"
  'aria-owns': [/^.*-\d+$/], // e.g. "mat-input-4"
  'aria-labelledby': [/^.*-\d+$/], // e.g. "mat-input-4", "cdk-step-label-0-0"
  'aria-controls': [/^.*-\d+$/], // e.g. "cdk-step-content-2-0"
};
const attributesToCleanKeys = Object.keys(attributesToClean);
const hasAttributesToClean = (attribute: Attr) => attributesToCleanKeys.some((name) => attribute.name === name);

let lastCleanedNode: Element | null = null;

module.exports = {
  print: (val: Element, serialize: (v: Element) => string) => {
    const clone = val.cloneNode(true) as Element;

    for (const attr of Object.values(clone.attributes).filter(hasAttributesToClean)) {
      attr.value = attr.value
        .split(' ')
        .filter((attrValue: string) => {
          return !attributesToClean[attr.name].some((regex) => regex.test(attrValue));
        })
        .join(' ');
    }

    lastCleanedNode = clone;
    return serialize(clone);
  },
  test: (val: Element) => val !== lastCleanedNode && val.attributes !== undefined && Object.values(val.attributes).some(hasAttributesToClean),
};

Does this exist in a repo or a gist somewhere, I'm trying to fight the root0, root1 problem in angular snapshots and the angular preset serializers appear to do nothing, or nothing for this problem.

Also what's the license on this code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

10 participants