Skip to content

Instantly share code, notes, and snippets.

@Bestra
Last active August 17, 2018 18:51
Show Gist options
  • Save Bestra/bf410400a9341b46bcfe60b38ea864e1 to your computer and use it in GitHub Desktop.
Save Bestra/bf410400a9341b46bcfe60b38ea864e1 to your computer and use it in GitHub Desktop.
Angular page object decorators
/**
* Creates a getter for the given css selector
* that will return a native element
* @param selector A css selector string
*/
export function element(selector: string) {
return function(target: any, key: string) {
delete target[key];
Object.defineProperty(target, key, {
get: function(this: Page) {
return this.query(selector);
},
enumerable: true,
configurable: true,
});
};
}
/**
* Creates a getter for the given css selector
* that will return an array of native elements
* @param selector A css selector string
*/
export function collection(selector: string) {
return function(target: any, key: string) {
delete target[key];
Object.defineProperty(target, key, {
get: function(this: Page) {
return this.queryAll(selector);
},
enumerable: true,
configurable: true,
});
};
}
class Page {
// These getters are the ones I'd like to make a little more streamlined
@collection('button') buttons: HTMLButtonElement;
@element('span') nameDisplay: HTMLElement;
@element('input') nameInput: HTMLInputElement;
get saveBtn() { return this.buttons[0]; }
get cancelBtn() { return this.buttons[1]; }
constructor(fixture: ComponentFixture<HeroDetailComponent>) {
// omitted for clarity
}
//// query helpers ////
query<T>(selector: string): T {
return fixture.nativeElement.querySelector(selector);
}
queryAll<T>(selector: string): T[] {
return fixture.nativeElement.querySelectorAll(selector);
}
}
/**
* Creates a getter for the given css selector
* that will return a native element
* @param selector A css selector string
*/
export function element(selector: string) {
return function(target: any, key: string) {
delete target[key];
Object.defineProperty(target, key, {
get: function(this: Page) {
return this.query(selector);
},
enumerable: true,
configurable: true,
});
};
}
/**
* Creates a getter for the given css selector
* that will return a native element
* @param selector A css selector string
*/
export function element(selector: string) {
return function(target: any, key: string) {
delete target[key];
Object.defineProperty(target, key, {
get: function(this: Page) {
return this.query(selector);
},
enumerable: true,
configurable: true,
});
};
}
class Page {
// These getters are the ones I'd like to make a little more streamlined
get buttons() { return this.queryAll<HTMLButtonElement>('button'); }
get saveBtn() { return this.buttons[0]; }
get cancelBtn() { return this.buttons[1]; }
@element('span') nameDisplay: HTMLElement;
@element('input') nameInput: HTMLInputElement;
constructor(fixture: ComponentFixture<HeroDetailComponent>) {
// omitted for clarity
}
//// query helpers ////
query<T>(selector: string): T {
return fixture.nativeElement.querySelector(selector);
}
queryAll<T>(selector: string): T[] {
return fixture.nativeElement.querySelectorAll(selector);
}
}
class Page {
// getter properties wait to query the DOM until called.
get buttons() { return this.queryAll<HTMLButtonElement>('button'); }
get saveBtn() { return this.buttons[0]; }
get cancelBtn() { return this.buttons[1]; }
@element('span') nameDisplay: HTMLElement;
@element('input') nameInput: HTMLInputElement;
//...snip
}
/**
* This Page takes its fixture and root element separately.
* Queries start from the root element rather than the fixture's nativeElement
*/
export class PageFragment {
fixture: ComponentFixture<any>;
rootElement: NativeElement;
constructor(fixture: ComponentFixture<any>, rootElement?: NativeElement) {
this.fixture = fixture;
this.rootElement = rootElement || fixture.nativeElement;
}
}
/**
* Embeds another Page object inside the current one
* @param selector The css selector for the fragment root
* @param klass The constructor for the fragment
*/
export function fragment(selector: string, klass: typeof PageFragment) {
return function(target: any, key: string) {
// instantiates a new PageFragment class with the current Page's fixture.
// finds the root for the new fragment via the `selector`
const getter = function(this: PageFragment) {
const root = this.querySelector(selector);
const f = new klass(this.fixture, root);
return f;
};
delete target[key];
Object.defineProperty(target, key, {
get: getter,
enumerable: true,
configurable: true,
});
};
}
class HeroForm extends PageFragment {
@element('[name=firstName]) firstName: HTMLElement;
@element('[name=lastName]) lastName: HTMLElement;
@element('[type=submit]') submitButton: HTMLElement
submit() {
this.submitButton.click();
}
}
class HeroEditPage extends PageFragment {
@element('h3') heroTitle: HTMLElement
@fragment('form', HeroForm) form: HeroForm;
finish() {
this.form.submit()
}
}
class Page {
// These getters are the ones I'd like to make a little more streamlined
get buttons() { return this.queryAll<HTMLButtonElement>('button'); }
get saveBtn() { return this.buttons[0]; }
get cancelBtn() { return this.buttons[1]; }
get nameDisplay() { return this.query<HTMLElement>('span'); }
get nameInput() { return this.query<HTMLInputElement>('input'); }
constructor(fixture: ComponentFixture<HeroDetailComponent>) {
// omitted for clarity
}
//// query helpers ////
private query<T>(selector: string): T {
return fixture.nativeElement.querySelector(selector);
}
private queryAll<T>(selector: string): T[] {
return fixture.nativeElement.querySelectorAll(selector);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment