*/ class EmogrifierTest extends \PHPUnit_Framework_TestCase { /** * @var string */ const LF = "\n"; /** * @var string */ private $html4TransitionalDocumentType = ''; /** * @var string */ private $xhtml1StrictDocumentType = ''; /** * @var string */ private $html5DocumentType = ''; /** * @var Emogrifier */ private $subject = null; /** * Sets up the test case. * * @return void */ protected function setUp() { $this->html4TransitionalDocumentType = ''; $this->xhtml1StrictDocumentType = ''; $this->subject = new Emogrifier(); } /** * @test * * @expectedException \BadMethodCallException */ public function emogrifyForNoDataSetReturnsThrowsException() { $this->subject->emogrify(); } /** * @test * * @expectedException \BadMethodCallException */ public function emogrifyForEmptyHtmlAndEmptyCssThrowsException() { $this->subject->setHtml(''); $this->subject->setCss(''); $this->subject->emogrify(); } /** * @test */ public function emogrifyByDefaultEncodesUmlautsAsHtmlEntities() { $html = $this->html5DocumentType . '
Einen schönen Gruß!
'; $this->subject->setHtml($html); $this->assertContains( 'Einen schönen Gruß!', $this->subject->emogrify() ); } /** * @test */ public function emogrifyCanKeepEncodedUmlauts() { $this->subject->preserveEncoding = true; $encodedString = 'Küss die Hand, schöne Frau.'; $html = $this->html5DocumentType . '' . $encodedString . '
'; $this->subject->setHtml($html); $this->assertContains( $encodedString, $this->subject->emogrify() ); } /** * @test */ public function emogrifyForHtmlTagOnlyAndEmptyCssReturnsHtmlTagWithHtml4DocumentType() { $html = ''; $this->subject->setHtml($html); $this->subject->setCss(''); $this->assertSame( $this->html4TransitionalDocumentType . self::LF . $html . self::LF, $this->subject->emogrify() ); } /** * @test */ public function emogrifyForHtmlTagWithXhtml1StrictDocumentTypeKeepsDocumentType() { $html = $this->xhtml1StrictDocumentType . self::LF . '' . self::LF; $this->subject->setHtml($html); $this->subject->setCss(''); $this->assertSame( $html, $this->subject->emogrify() ); } /** * @test */ public function emogrifyForHtmlTagWithXhtml5DocumentTypeKeepsDocumentType() { $html = $this->html5DocumentType . self::LF . '' . self::LF; $this->subject->setHtml($html); $this->subject->setCss(''); $this->assertSame( $html, $this->subject->emogrify() ); } /** * @test */ public function emogrifyByDefaultRemovesWbrTag() { $html = $this->html5DocumentType . self::LF . 'foo', $this->subject->emogrify() ); } /** * @test */ public function addUnprocessableTagNotRemovesTagWithContent() { $this->subject->addUnprocessableHtmlTag('p'); $html = $this->html5DocumentType . self::LF . '
foobar
' . self::LF; $this->subject->setHtml($html); $this->assertContains( '', $this->subject->emogrify() ); } /** * @test */ public function removeUnprocessableHtmlTagCausesTagToStayAgain() { $this->subject->addUnprocessableHtmlTag('p'); $this->subject->removeUnprocessableHtmlTag('p'); $html = $this->html5DocumentType . self::LF . '
foo
bar
', $this->subject->emogrify() ); } /** * @test */ public function emogrifyCanAddMatchingElementRuleOnHtmlElementFromCss() { $html = $this->html5DocumentType . self::LF . '' . self::LF; $this->subject->setHtml($html); $styleRule = 'color: #000;'; $this->subject->setCss('html {' . $styleRule . '}'); $this->assertContains( '', $this->subject->emogrify() ); } /** * @test */ public function emogrifyNotAddsNotMatchingElementRuleOnHtmlElementFromCss() { $html = $this->html5DocumentType . self::LF . '' . self::LF; $this->subject->setHtml($html); $this->subject->setCss('p {color:#000;}'); $this->assertContains( '', $this->subject->emogrify() ); } /** * @test */ public function emogrifyCanMatchTwoElements() { $html = $this->html5DocumentType . self::LF . '
' . self::LF; $this->subject->setHtml($html); $styleRule = 'color: #000;'; $this->subject->setCss('p {' . $styleRule . '}'); $this->assertSame( 2, substr_count($this->subject->emogrify(), '') ); } /** * @test */ public function emogrifyCanAssignTwoStyleRulesFromSameMatcherToElement() { $html = $this->html5DocumentType . self::LF . '
' . self::LF; $this->subject->setHtml($html); $styleRulesIn = 'color:#000; text-align:left;'; $styleRulesOut = 'color: #000; text-align: left;'; $this->subject->setCss('p {' . $styleRulesIn . '}'); $this->assertContains( '', $this->subject->emogrify() ); } /** * @test */ public function emogrifyCanMatchAttributeOnlySelector() { $html = $this->html5DocumentType . self::LF . '
' . self::LF; $this->subject->setHtml($html); $this->subject->setCss('[hidden] { color:red; }'); $this->assertContains( '', $this->subject->emogrify() ); } /** * @test */ public function emogrifyCanAssignStyleRulesFromTwoIdenticalMatchersToElement() { $html = $this->html5DocumentType . self::LF . '
' . self::LF; $this->subject->setHtml($html); $styleRule1 = 'color: #000;'; $styleRule2 = 'text-align: left;'; $this->subject->setCss('p {' . $styleRule1 . '} p {' . $styleRule2 . '}'); $this->assertContains( '', $this->subject->emogrify() ); } /** * @test */ public function emogrifyCanAssignStyleRulesFromTwoDifferentMatchersToElement() { $html = $this->html5DocumentType . self::LF . '
' . self::LF; $this->subject->setHtml($html); $styleRule1 = 'color: #000;'; $styleRule2 = 'text-align: left;'; $this->subject->setCss('p {' . $styleRule1 . '} .x {' . $styleRule2 . '}'); $this->assertContains( '', $this->subject->emogrify() ); } /** * Data provide for selectors. * * @return array[] */ public function selectorDataProvider() { $styleRule = 'color: red;'; $styleAttribute = 'style="' . $styleRule . '"'; return array( 'universal selector HTML' => array('* {' . $styleRule . '} ', '##'), 'universal selector BODY' => array('* {' . $styleRule . '} ', '#
#'), 'universal selector P' => array('* {' . $styleRule . '} ', '#]*' . $styleAttribute . '>#'), 'type selector matches first P' => array('p {' . $styleRule . '} ', '#
#'), 'type selector matches second P' => array('p {' . $styleRule . '} ', '#
#'),
'descendant selector P SPAN'
=> array('p span {' . $styleRule . '} ', '##'),
'descendant selector BODY SPAN'
=> array('body span {' . $styleRule . '} ', '##'),
'child selector P > SPAN matches direct child'
=> array('p > span {' . $styleRule . '} ', '##'),
'child selector BODY > SPAN not matches grandchild'
=> array('body > span {' . $styleRule . '} ', '##'),
'adjacent selector P + P not matches first P' => array('p + p {' . $styleRule . '} ', '# #'),
'adjacent selector P + P matches second P'
=> array('p + p {' . $styleRule . '} ', '# #'),
'adjacent selector P + P matches third P'
=> array('p + p {' . $styleRule . '} ', '# #'),
'ID selector #HTML' => array('#html {' . $styleRule . '} ', '##'),
'type and ID selector HTML#HTML'
=> array('html#html {' . $styleRule . '} ', '##'),
'class selector .P-1' => array('.p-1 {' . $styleRule . '} ', '# #'),
'type and class selector P.P-1'
=> array('p.p-1 {' . $styleRule . '} ', '# #'),
'attribute presence selector SPAN[title] matches element with matching attribute'
=> array('span[title] {' . $styleRule . '} ', '##'),
'attribute presence selector SPAN[title] not matches element without any attributes'
=> array('span[title] {' . $styleRule . '} ', '##'),
'attribute value selector SPAN[title] matches element with matching attribute value' => array(
'span[title="bonjour"] {' . $styleRule . '} ', '##'
),
'attribute value selector SPAN[title] not matches element with other attribute value'
=> array('span[title="bonjour"] {' . $styleRule . '} ', '##'),
'attribute value selector SPAN[title] not matches element without any attributes'
=> array('span[title="bonjour"] {' . $styleRule . '} ', '##'),
);
}
/**
* @test
*
* @param string $css the complete CSS
* @param string $containedHtml regular expression for the the HTML that needs to be contained in the merged HTML
*
* @dataProvider selectorDataProvider
*/
public function emogrifierMatchesSelectors($css, $containedHtml)
{
$html = $this->html5DocumentType . self::LF .
'' .
' ' .
' some text some text some more text target target target target paragraph ';
$this->subject->setHtml($html);
$this->subject->disableStyleBlocksParsing();
$this->assertContains(
$expected,
$this->subject->emogrify()
);
}
/**
* @test
*/
public function emogrifyWhenDisabledNotAppliesCssFromInlineStyles()
{
$styleAttributeValue = 'color: #ccc;';
$html = $this->html5DocumentType . self::LF .
'';
$expected = '';
$this->subject->setHtml($html);
$this->subject->disableInlineStyleAttributesParsing();
$this->assertContains(
$expected,
$this->subject->emogrify()
);
}
/**
* @test
*/
public function emogrifyWhenInlineStyleAttributesParsingDisabledKeepStyleBlockStyles()
{
$styleAttributeValue = 'color: #ccc;';
$html = $this->html5DocumentType . self::LF .
' paragraph ';
$this->subject->setHtml($html);
$this->subject->disableInlineStyleAttributesParsing();
$this->assertContains(
$expected,
$this->subject->emogrify()
);
}
/**
* @test
*/
public function emogrifyAppliesCssWithUpperCaseSelector()
{
$html = $this->html5DocumentType . self::LF .
' paragraph ';
$this->subject->setHtml($html);
$this->assertContains(
$expected,
$this->subject->emogrify()
);
}
/**
* Emogrify was handling case differently for passed in CSS vs CSS parsed from style blocks.
* @test
*/
public function emogrifyAppliesCssWithMixedCaseAttributesInStyleBlock()
{
$html = $this->html5DocumentType . self::LF .
' some content ';
$this->subject->setHtml($html);
$this->assertContains(
$expected,
$this->subject->emogrify()
);
}
/**
* Passed in CSS sets the order, but style block CSS overrides values.
* @test
*/
public function emogrifyMergesCssWithMixedCaseAttribute()
{
$css = 'p { margin: 0; padding-TOP: 0; PADDING-bottom: 1PX;}';
$html = $this->html5DocumentType . self::LF .
' some content ';
$this->subject->setHtml($html);
$this->subject->setCss($css);
$this->assertContains(
$expected,
$this->subject->emogrify()
);
}
/**
* @test
*/
public function emogrifyMergesCssWithMixedUnits()
{
$css = 'p { margin: 1px; padding-bottom:0;}';
$html = $this->html5DocumentType . self::LF .
' some content ';
$this->subject->setHtml($html);
$this->subject->setCss($css);
$this->assertContains(
$expected,
$this->subject->emogrify()
);
}
}